Compare commits

...

93 Commits

Author SHA1 Message Date
a07eb07090 Set the mode of the independent time conductor timecontext so that it's clock can tick. 2023-06-21 21:16:18 -07:00
9f55e4c98e WIP 2023-06-20 12:54:55 -07:00
17a1a74eed stil WIP, but progress made on independent and regular conductor popup finctionality, next steps polishing api 2023-06-15 11:58:10 -07:00
ec95faa57f WIP 2023-06-01 11:35:49 -07:00
488902f436 various changes, but mainly changes to independent time context 2023-05-24 16:34:43 -07:00
0db9a5e2a7 cleanup 2023-05-24 11:30:57 -07:00
1950839f80 itc work, moving mutating the domain object into itc so its in one place 2023-05-24 11:29:01 -07:00
0ddb49c11d forgot one 2023-05-24 09:10:45 -07:00
5f6cf7b0df using constants for events in the conductor components 2023-05-24 09:09:52 -07:00
295acdb36b cleaning up some items 2023-05-23 21:30:55 -07:00
d68a58f7e0 updated xAxis to new api, it was thrwoing many warnings for calling .clock, may want to look into that 2023-05-23 13:41:41 -07:00
4a2d903050 realtime working on load 2023-05-23 13:23:48 -07:00
7aa3762684 WIP 2023-05-22 16:05:13 -07:00
74cfab0ec0 working on independent time conductor part 2023-05-22 09:46:33 -07:00
1ebf566bb7 WIP: lots of updates to methods and moving component creation from programatic to v-ifs, mostly focused on conductor at the moment, independent next 2023-05-18 17:07:34 -07:00
784113012e moved conductor popup into conductor instead of creating the component on the fly, this way we are prepping for vue 3 and we can use v-if to destroy it 2023-05-10 12:50:09 -07:00
0260e06222 various changes for setting modes and clocks, also updates to conductor history for new api methods 2023-05-10 11:22:11 -07:00
266470a755 WIP 2023-05-09 17:34:48 -07:00
d851b01363 WIP 2023-05-02 12:05:40 -07:00
f41df68458 Merge branch 'time-api-enhancements' into mode-dropdown
Merging in shefali's time api updatesy
2023-04-28 13:41:19 -07:00
ac5c579874 WIP 2023-04-28 12:34:25 -07:00
204fb8c0e0 manually cherry picking over my changes to this existing branch 2023-04-26 14:32:42 -07:00
c605cd7a17 Stubs for the new Time API methods 2023-04-25 21:43:15 -07:00
a5db0f3b71 Ensure that clock changes are reflected downstream 2023-04-25 09:06:15 -07:00
62483583eb Ensure independent time conductor mode works as expected 2023-04-24 21:46:28 -07:00
6c63773641 object path for independent time conductor 2023-04-24 20:19:23 -07:00
e27e315784 Independent time conductor popup draft 2023-04-24 13:18:31 -07:00
5dc82742bf Add independent time conductor popup logic 2023-04-21 15:48:35 -07:00
bd4d30f481 Ensure conductor history works when mode is switched. Submit and cancel work as expected 2023-04-19 21:03:26 -07:00
b8322a8311 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-19 14:35:07 -07:00
c200999659 test(e2e): fix overlay plot element preview test flake (#6609)
test: wait for plot to be drawn before assertions
2023-04-19 13:40:44 -07:00
ddeeff4822 Fix test flake and playwright version in couchdb circleci test (#6608)
* test: wait for `domcontentloaded` on reload

* chore: playwright `1.32.3` in circleci couchdb test

* test: simplify(why was this creating a clock??)
2023-04-19 19:11:09 +00:00
5610846147 chore(deps-dev): bump @deploysentinel/playwright from 0.3.3 to 0.3.4 (#6606)
Bumps @deploysentinel/playwright from 0.3.3 to 0.3.4.

---
updated-dependencies:
- dependency-name: "@deploysentinel/playwright"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-19 17:17:57 +00:00
88fde47932 Add @types/lodash to dependabot semver ignore list (#6604)
Add lodash types to ignore list
2023-04-19 09:23:16 -07:00
2d6c6a6b38 Save fixed time bounds 2023-04-18 23:08:25 -07:00
f4e747a85e Ensure toggling between fixed timespan and clock modes works. Save clock offsets. 2023-04-18 21:51:19 -07:00
2a0faba35f chore(deps-dev): bump eslint-plugin-vue from 9.10.0 to 9.11.0 (#6597)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.10.0 to 9.11.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.10.0...v9.11.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  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>
2023-04-18 16:41:58 -07:00
a47abf5f96 chore(deps-dev): bump webpack-dev-server from 4.11.1 to 4.13.3 (#6596)
* chore(deps-dev): bump webpack-dev-server from 4.11.1 to 4.13.3

Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.11.1 to 4.13.3.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.11.1...v4.13.3)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* fix: disable webpack overlay for runtime errors

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-04-18 16:33:43 -07:00
968eee6698 chore: bump Playwright to v1.32.3 (#6511)
* chore: bump Playwright to v1.32.1

* test: fix locators, remove unnecessary awaits

* chore: bump Playwright in ci workflows

* test: better selectors for yAxis configs

- fix tests

* chore: bump Playwright to 1.32.3

* refactor: ensure openmct starts after plugins install

* fix: wait for domcontentloaded on initial nav

* test: fix autoscale snapshot test

* test: fix `--max-failures` argname typo

* test: update old locators

* test(fix): add missing await

* test: fix typo 😅
2023-04-18 22:32:29 +00:00
43d56a68bb Bump for 2.2.3 (#6600)
Update package.json

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-04-18 21:13:05 +00:00
f055a8a0c7 fix(e2e): remove unnecessary wait for networkidle and fix selectors (#6370) 2023-04-18 20:25:43 +00:00
9ccdbcede8 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-18 11:12:38 -07:00
2820237d60 Fixes the way we start and stop couchdb on bare metal CI agents (#6589) 2023-04-17 14:26:13 -07:00
2e04c686f4 Initial work on getting the conductor popup working 2023-04-17 13:36:05 -07:00
dbdc9bb4e2 fix(#6549): [StaticRootPlugin] Remap non-empty root namespaces and non-root namespaces correctly (#6583)
* fix: preserve truthy namespaces and unmapped values

- Fixes the issue of the Static Root provider overriding existing namespaces, such as those from a telemetry dictionary
- Fixes the issue of keys of child objects NOT present in the idMapping (such as those from a telemetry dictionary) being overwritten as undefined
- TODO: This will not work for objects exported from an environment that has the "MyItems" namespace defined to anything other than an empty string. Need to figure out how to handle this.

* fix: handle the case of rootId having a namespace !== ""

* refactor: use `parseKeyString`

* fix: StaticRootPlugin object mapping for non-empty namespaces

* fix: use index, fix location identifiers

* tets: add non-empty namespace tests (wip)

* refactor: rename and move test files

* test: update StaticModelProvider tests

* fix: remap to identifiers for config, not keystring

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2023-04-14 18:31:04 -05:00
a9a98380f2 test(visual): add theme to notification banner test name (#6450)
* test(visual): add theme to notification banner test name

* test: rename file

* test: switch to small example plan

* test: snapshot object-view only

* test: fix selectors

* refactor: chain locators together
2023-04-13 10:02:56 -07:00
e3ab085dd5 chore(deps-dev): bump webpack from 5.78.0 to 5.79.0 (#6588)
Bumps [webpack](https://github.com/webpack/webpack) from 5.78.0 to 5.79.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.78.0...v5.79.0)

---
updated-dependencies:
- dependency-name: webpack
  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>
2023-04-12 15:38:59 -07:00
519135527b chore(deps-dev): bump sass from 1.61.0 to 1.62.0 (#6587)
Bumps [sass](https://github.com/sass/dart-sass) from 1.61.0 to 1.62.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.61.0...1.62.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-12 13:44:16 -07:00
fc37f6e05b Move Dependabot to weekly to reduce frequency of changes and reduce CI Bill (#6585)
Update dependabot.yml
2023-04-12 11:11:41 -07:00
ab1df89396 chore(deps-dev): bump webpack from 5.77.0 to 5.78.0 (#6559)
Bumps [webpack](https://github.com/webpack/webpack) from 5.77.0 to 5.78.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.77.0...v5.78.0)

---
updated-dependencies:
- dependency-name: webpack
  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>
2023-04-11 12:25:59 -07:00
9ee5ab96f3 chore(deps-dev): bump sass from 1.59.3 to 1.61.0 (#6569)
Bumps [sass](https://github.com/sass/dart-sass) from 1.59.3 to 1.61.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.59.3...1.61.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-11 12:09:45 -07:00
8b2c6e3fb3 chore(deps-dev): bump @percy/cli from 1.22.0 to 1.23.0 (#6578)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.23.0/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  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>
2023-04-11 08:15:48 -07:00
b8b0a08eeb Add Code Coverage Doc and some other drive-bys (#5724) 2023-04-10 12:25:17 -07:00
633b6be2fd chore(deps-dev): bump typescript from 4.9.5 to 5.0.4 (#6574)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.0.4)

---
updated-dependencies:
- dependency-name: typescript
  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>
2023-04-10 11:21:16 -07:00
4963aff8a0 Only decrement and save if there is composition but no child object reference (#6568)
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-04-07 06:17:52 +00:00
6786be54fa Add safari as well as iOS safari browserlist check (#6567)
* Add safari as well as iOS safari browserlist check

* Update package.json

* math
2023-04-06 16:51:28 -07:00
508c2ebd87 Closes #4975
- CSS fix for to-be-deprecated division operation.
2023-04-06 15:56:09 -07:00
b081389e68 chore(deps-dev): bump @percy/cli from 1.21.0 to 1.22.0 (#6565)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.22.0/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  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>
2023-04-06 14:34:59 -07:00
892963aa0e Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-06 09:38:13 -07:00
7a3ec3a241 Fix ExportAsJSONAction to not lose layout configurations on linked objects (#6562) 2023-04-05 23:03:23 -07:00
c0c383bf18 First attempt at DeploySentinel and fix couchdb notebook tests (#6398)
* First attempt

* Remove commented pattern

* Add deploysentinel to github action

* drive by

* Stablization

* remove only

* entries now selected on creation

* select previous entry on deletion

* add deletion test

* wip

* fix adding focus selection

* remove previous entry selection logic

* null check for event

* address review comments

* address review comments

* refactor tests a bit

* typo

* Add some determinism to avoid console errors

* refactor and use methods

* stabilize

* remove debug

* remove only

* combine clean commands

* comments

* change to expects

* test: await toBeHidden() assertion

* test: use `myItemsFolderName` instead of 'My Items'

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-04-05 06:25:28 -07:00
fe1c99de12 First attempt at DeploySentinel and fix couchdb notebook tests (#6398)
* First attempt

* Remove commented pattern

* Add deploysentinel to github action

* drive by

* Stablization

* remove only

* entries now selected on creation

* select previous entry on deletion

* add deletion test

* wip

* fix adding focus selection

* remove previous entry selection logic

* null check for event

* address review comments

* address review comments

* refactor tests a bit

* typo

* Add some determinism to avoid console errors

* refactor and use methods

* stabilize

* remove debug

* remove only

* combine clean commands

* comments

* change to expects

* test: await toBeHidden() assertion

* test: use `myItemsFolderName` instead of 'My Items'

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-04-05 06:24:57 -07:00
2e60da0401 Ensure annotations on empty entries in notebook are not lost (#6525)
* entries now selected on creation

* select previous entry on deletion

* add deletion test

* wip

* fix adding focus selection

* remove previous entry selection logic

* null check for event

* address review comments

* address review comments

* refactor tests a bit

* typo

* remove clicking on entries
2023-04-04 14:37:38 -07:00
bc3a5408b4 fix(#6503): Recently Viewed Items - Disable button if no items (#6533)
* add e2e test

* fix: remove slow function

* test: After deactivating the button, new objects must be inserted and the button becomes active again

* test: ensure clear recent objects button is active or inactive

* add an event to notify when an object is inserted in the recents list

* add an event to notify when an object is inserted in the recents list

* fix: adjusting function name and add validation for triggering the event

* fix: add event to disable button only when user click ok to clear list of recents

* test: fix failing e2e + better assertions

---------

Co-authored-by: Jesse Mazzella <jessemazzella@gmail.com>
2023-04-03 23:56:03 +00:00
344bf8eed3 [CI] Stop taking patch releases for moment-timezone (#6553)
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-04-03 17:20:09 +00:00
cbb3368937 chore: bump version to 2.2.2-SNAPSHOT (#6552)
Update package.json
2023-04-03 09:06:24 -07:00
b7a671d392 Allow Restricted Notebooks to export text (#6542)
* Refactor string to stream

* add to restricted notebooks and allow for blank users

* forgot to add notebook

* use better types and fix test

* move streamToString

* add export group

* catch blank pages
2023-04-01 07:08:22 +02:00
4f10a93ef5 Use node-version: 'lts/gallium' (#6540)
* Update e2e-couchdb.yml

* this really doesn't like it

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-31 11:55:20 -07:00
f8186e4b4e chore(deps-dev): bump karma-sourcemap-loader from 0.3.8 to 0.4.0 (#6290)
Bumps [karma-sourcemap-loader](https://github.com/demerzel3/karma-sourcemap-loader) from 0.3.8 to 0.4.0.
- [Release notes](https://github.com/demerzel3/karma-sourcemap-loader/releases)
- [Changelog](https://github.com/demerzel3/karma-sourcemap-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/demerzel3/karma-sourcemap-loader/commits/0.4.0)

---
updated-dependencies:
- dependency-name: karma-sourcemap-loader
  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>
2023-03-31 02:53:09 -07:00
4e0c364d89 chore(deps-dev): bump sass-loader from 13.2.1 to 13.2.2 (#6513)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 13.2.1 to 13.2.2.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v13.2.1...v13.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-31 04:04:35 +00:00
f3bed9c651 chore(deps-dev): bump webpack from 5.76.3 to 5.77.0 (#6520)
Bumps [webpack](https://github.com/webpack/webpack) from 5.76.3 to 5.77.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.76.3...v5.77.0)

---
updated-dependencies:
- dependency-name: webpack
  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>
2023-03-30 20:54:11 -07:00
4d93907d58 chore(deps-dev): bump eslint-plugin-compat from 4.1.1 to 4.1.2 (#6352)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-compat
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 15:37:59 -07:00
6f656a6783 [Build]Remove Node14LTS from supported versions and update our pipelines (#6527)
* bump minimumum and pin drivebys

* move to 16 and remove 14

* remove 14

* update to latest
2023-03-30 20:53:44 +00:00
767fb6c5fd fix: remove redundant request on FaultManagement mount (#6502)
* fix: remove redundant update request

* fix: handle case where request returns no faults

* test: fix fault management tests

* docs: clean up FaultManagement API types

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-03-30 18:43:55 +00:00
b0a0b4bb58 chore(deps-dev): bump eslint from 8.36.0 to 8.37.0 (#6521)
Bumps [eslint](https://github.com/eslint/eslint) from 8.36.0 to 8.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.36.0...v8.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 11:15:36 -07:00
340f4a9e79 chore(deps-dev): bump @percy/cli from 1.17.0 to 1.21.0 (#6439)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.17.0 to 1.21.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.21.0/packages/cli)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 11:05:32 -07:00
3007b28b0f Simple text export of Notebook (#6510)
* add simple prototype

* tags and metadata now exported

* add form for options

* revert notebook

* add simple e2e test

* add test stubs

* death to debug
2023-03-30 19:44:12 +02:00
20789601b4 Add contextual domain object back for contextual row actions (#6524)
* re-enable historical row action
2023-03-30 10:08:09 -07:00
a56cfed732 Remove ticker and rely solely on the clock ticks to update the timelist durations (#6495)
* Remove ticker for timelist and rename a function for readability

* Use formatting for remote clock if available.

* throttle updates to the timestamp and listing activities

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-03-29 15:16:52 -07:00
34864771b3 Merge branch 'time-conductor-4975' of https://github.com/nasa/openmct into time-conductor-4975 2022-11-03 13:40:57 -07:00
2f1eb7f1bc Fixes #4975 - Compact Time Conductor styling
- Styling for Time Conductor in layout frames.
2022-05-17 23:08:03 -07:00
2506dfb25d Fixes #4975 - Compact Time Conductor styling
- Convert Time Conductor symbol to use SVG instead of font glyph
2022-05-17 22:33:29 -07:00
6ec07490ff Fixes #4975 - Compact Time Conductor styling
- Layout, display behavior for Time Conductor in layout frames.
- Code cleanups.
2022-05-17 22:07:31 -07:00
04c76a0d0d Fixes #4975 - Compact Time Conductor styling
- Fix SCSS error.
2022-05-17 13:24:13 -07:00
e5d701dea2 Fixes #4975 - Compact Time Conductor styling
- Layout, display behavior.
- Hide functional buttons to be moved.
- Remove unneeded markup.
- Input widths for fixed popup date and time.
- Add new `c-not-button` class.
- Add new `u-flex-spreader` class.
- Code cleanups.
2022-05-17 13:21:03 -07:00
f22f826546 Fixes #4975 - Compact Time Conductor styling
- Stubbed buttons into popups.
- More `$colorTime*` theme constants defined and applied.
- Still quite WIP!
2022-05-16 23:03:30 -07:00
fc83d88670 Fixes #4975 - Compact Time Conductor styling
- Fixed inputs popup layout with style and layout.
- More `$colorTime*` theme constants defined and applied.
- Better CSS organization.
2022-05-16 22:31:04 -07:00
47e4c3af67 Fixes #4975 - Compact Time Conductor styling
- Styling for new mini toggle slider switch.
2022-05-16 18:43:29 -07:00
d0cc125867 Fixes #4975 - Compact Time Conductor styling
- Markup and script moved into new timePopup* components.
- Significant work on styling.
- Theme constants augmented, better naming.
2022-05-16 18:42:58 -07:00
65be53ba18 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2022-05-16 10:42:09 -07:00
1e302e9f5e Fixes #4975 - Compact Time Conductor styling
- Added CSS class `is-expanded` to main view TC component.
2022-05-13 17:28:37 -07:00
9e174c40ed Fixes #4975 - Compact Time Conductor styling
- Significant CSS and markup work.
- Refinements to `c-icon-button` classes, including new `--compact` definition.
- Styling for independent and main conductor components.
- Code cleanups.
- STILL WIP!
2022-05-13 17:23:17 -07:00
453311272e Fixed #4975 - Compact Time Conductor styling
- Moved IndependentTimeConductor.vue into BrowseBar.vue.
- Styling for read-only conductor views.
- VERY WIP!
2022-05-13 12:07:19 -07:00
136 changed files with 4414 additions and 2073 deletions

View File

@ -2,11 +2,15 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.29.0-focal
- image: mcr.microsoft.com/playwright:v1.32.3-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
ubuntu:
machine:
image: ubuntu-2204:current
docker_layer_caching: true
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
@ -23,9 +27,8 @@ commands:
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- run: npm install --prefer-offline --no-audit --progress=false
- run: npm install --no-audit --progress=false
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
@ -37,7 +40,7 @@ commands:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
parameters:
@ -45,7 +48,7 @@ commands:
type: string
steps:
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
@ -53,8 +56,8 @@ commands:
description: "Track important packages and files"
steps:
- run: |
mkdir /tmp/artifacts
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt || true
npm -v >> /tmp/artifacts/npm-version.txt
node -v >> /tmp/artifacts/node-version.txt
ls -latR >> /tmp/artifacts/dir.txt
@ -69,7 +72,7 @@ commands:
- run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@4.9.0
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
@ -110,7 +113,11 @@ jobs:
path: dist/reports/tests/
- store_artifacts:
path: coverage
- generate_and_store_version_and_filesystem_artifacts
- when:
condition:
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
node-version:
@ -128,8 +135,12 @@ jobs:
steps:
- run: npx playwright install chrome-beta
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- generate_e2e_code_cov_report:
suite: <<parameters.suite>>
- when:
condition:
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_e2e_code_cov_report:
suite: <<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@ -138,7 +149,46 @@ jobs:
path: coverage
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
- when:
condition:
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
parameters:
node-version:
type: string
executor: ubuntu
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.32.3 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
sleep 3
bash src/plugins/persistence/couch/setup-couchdb.sh
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB
- run: npm run test:e2e:couchdb
- when:
condition:
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_e2e_code_cov_report:
suite: full #add to full suite
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
perf-test:
parameters:
node-version:
@ -154,7 +204,11 @@ jobs:
path: test-results
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
- when:
condition:
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-test:
parameters:
node-version:
@ -170,46 +224,49 @@ jobs:
path: test-results
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
- when:
condition:
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node14-lint
node-version: lts/fermium
name: node16-lint
node-version: lts/gallium
- unit-test:
name: node18-chrome
node-version: "18"
node-version: lts/hydrogen
- e2e-test:
name: e2e-stable
node-version: lts/gallium
node-version: lts/hydrogen
suite: stable
- perf-test:
node-version: lts/gallium
node-version: lts/hydrogen
- visual-test:
node-version: lts/gallium
node-version: lts/hydrogen
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
name: node14-chrome-nightly
node-version: lts/fermium
- unit-test:
name: node16-chrome-nightly
node-version: lts/gallium
- unit-test:
name: node18-chrome
node-version: "18"
node-version: lts/hydrogen
- npm-audit:
node-version: lts/gallium
node-version: lts/hydrogen
- e2e-test:
name: e2e-full-nightly
node-version: lts/gallium
node-version: lts/hydrogen
suite: full
- perf-test:
node-version: lts/gallium
node-version: lts/hydrogen
- visual-test:
node-version: lts/gallium
node-version: lts/hydrogen
- e2e-couchdb:
node-version: lts/hydrogen
triggers:
- schedule:
cron: "0 0 * * *"

View File

@ -4,13 +4,13 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "pr:daveit"
- "pr:e2e"
- "type:maintenance"
- "dependencies"
- "pr:daveit"
- "pr:platform"
ignore:
#We have to source the playwright container which is not detected by Dependabot
@ -25,11 +25,15 @@ updates:
update-types: ["version-update:semver-patch"]
- dependency-name: "sinon"
update-types: ["version-update:semver-patch"]
- dependency-name: "moment-timezone"
update-types: ["version-update:semver-patch"]
- dependency-name: "@types/lodash"
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "pr:daveit"
- "type:maintenance"
- "dependencies"
- "pr:daveit"

View File

@ -5,34 +5,39 @@ on:
types:
- labeled
- opened
env:
OPENMCT_DATABASE_NAME: openmct
COUCH_ADMIN_USER: admin
COUCH_ADMIN_PASSWORD: password
COUCH_BASE_LOCAL: http://localhost:5984
COUCH_NODE_NAME: nonode@nohost
jobs:
e2e-couchdb:
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run : docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
- run : sleep 3 # wait until CouchDB has started (TODO: there must be a better way)
- run : bash src/plugins/persistence/couch/setup-couchdb.sh
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
node-version: 'lts/gallium'
- run: npx playwright@1.32.3 install
- run: npm install
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb
- run: ls -latr
- name: Start CouchDB Docker Container and Init with Setup Scripts
run : |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
sleep 3
bash src/plugins/persistence/couch/setup-couchdb.sh
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- name: Run CouchDB Tests and publish to deploysentinel
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
run: npm run test:e2e:couchdb
- name: Publish Results to Codecov.io
env:
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
run: npm run cov:e2e:full:publish
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Archive html test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: html-test-results

View File

@ -5,7 +5,6 @@ on:
types:
- labeled
- opened
jobs:
e2e-full:
if: ${{ github.event.label.name == 'pr:e2e' }}
@ -30,11 +29,18 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npx playwright@1.32.3 install
- run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full
- run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true
- shell: bash
env:
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
run: |
npm run cov:e2e:full:publish
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results

View File

@ -1,21 +0,0 @@
name: "e2e"
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- name: Run the e2e tests
run: npm run test:e2e:ci

View File

@ -16,7 +16,6 @@ jobs:
- macos-latest
- windows-latest
node_version:
- 14
- 16
- 18
architecture:

View File

@ -53,7 +53,11 @@ module.exports = merge(common, {
},
client: {
progress: true,
overlay: true
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
}
});

View File

@ -191,7 +191,7 @@ The following guidelines are provided for anyone contributing source code to the
if (responseCode === 401)
```
1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases.
1. Test specs should reside alongside the source code they test, not in a separate directory.
1. Unit Test specs should reside alongside the source code they test, not in a separate directory.
1. Organize code by feature, not by type.
eg.
```
@ -222,44 +222,6 @@ The following guidelines are provided for anyone contributing source code to the
Deviations from Open MCT code style guidelines require two-party agreement,
typically from the author of the change and its reviewer.
### Test Standards
Automated testing shall occur whenever changes are merged into the main
development branch and must be confirmed alongside any pull request.
Automated tests are tests which exercise plugins, API, and utility classes.
Tests are subject to code review along with the actual implementation, to
ensure that tests are applicable and useful.
Examples of useful tests:
* Tests which replicate bugs (or their root causes) to verify their
resolution.
* Tests which reflect details from software specifications.
* Tests which exercise edge or corner cases among inputs.
* Tests which verify expected interactions with other components in the
system.
#### Guidelines
* 100% statement coverage is achievable and desirable.
* Do blackbox testing. Test external behaviors, not internal details. Write tests that describe what your plugin is supposed to do. How it does this doesn't matter, so don't test it.
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
* Where builtin functions have been mocked, be sure to clear them between tests.
* Test at an appropriate level of isolation. Eg.
* If youre testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
* You do not need to test that the view switcher works, there should be separate tests for that.
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
* Use your best judgement when deciding on appropriate scope.
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
* If writing unit tests for legacy Angular code be sure to follow [best practices in order to avoid memory leaks](https://www.thecodecampus.de/blog/avoid-memory-leaks-angularjs-unit-tests/).
#### Examples
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
### Commit Message Standards
Commit messages should:
@ -301,7 +263,7 @@ Issue severity is categorized as follows (in ascending order):
* _Trivial_: Minimal impact on the usefulness and functionality of the software; a "nice-to-have." Visual impact without functional impact,
* _Medium_: Some impairment of use, but simple workarounds exist
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though.
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though. Complex workarounds exist.
* _Blocker_: Major functionality is impaired or lost, threatening mission success. Display of telemetry data is impaired or blocked by the bug, which could lead to loss of situational awareness.
## Check Lists
@ -310,22 +272,4 @@ The following check lists should be completed and attached to pull requests
when they are filed (author checklist) and when they are merged (reviewer
checklist).
### Author Checklist
[Within PR Template](.github/PULL_REQUEST_TEMPLATE.md)
### Reviewer Checklist
* [ ] Changes appear to address issue?
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate unit tests included?
* [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards?
* [ ] Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)
* [ ] Has associated issue been labelled `bug`? (only applicable if this PR is for a bug fix)
* [ ] List of Acceptance Tests Performed.
Write out a small list of tests performed with just enough detail for another developer on the team
to execute.
i.e. ```When Clicking on Add button, new `object` appears in dropdown.```

50
TESTING.md Normal file
View File

@ -0,0 +1,50 @@
# Testing
Open MCT Testing is iterating and improving at a rapid pace. This document serves to capture and index existing testing documentation and house documentation which no other obvious location as our testing evolves.
## General Testing Process
Documentation located [here](./docs/src/process/testing/plan.md)
## Unit Testing
Unit testing is essential part of our test strategy and complements our e2e testing strategy.
#### Unit Test Guidelines
* Unit Test specs should reside alongside the source code they test, not in a separate directory.
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
* Where builtin functions have been mocked, be sure to clear them between tests.
* Test at an appropriate level of isolation. Eg.
* If youre testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
* You do not need to test that the view switcher works, there should be separate tests for that.
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
* Use your best judgement when deciding on appropriate scope.
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
#### Unit Test Examples
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
#### Unit Testing Execution
The unit tests can be executed in one of two ways:
`npm run test` which runs the entire suite against headless chrome
`npm run test:debug` for debugging the tests in realtime in an active chrome session.
## e2e, performance, and visual testing
Documentation located [here](./e2e/README.md)
## Code Coverage
* 100% statement coverage is achievable and desirable.
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
### Limitations in our code coverage reporting
Our code coverage implementation has two known limitations:
- [Variability and accuracy](https://github.com/nasa/openmct/issues/5811)
- [Vue instrumentation](https://github.com/nasa/openmct/issues/4973)

View File

@ -15,14 +15,14 @@ coverage:
flags:
unit:
carryforward: true
e2e-ci:
carryforward: true
carryforward: false
e2e-stable:
carryforward: false
e2e-full:
carryforward: true
comment:
layout: "reach,diff,flags,files,footer"
layout: "diff,flags,files,footer"
behavior: default
require_changes: false
show_carryforward_flags: true

View File

@ -200,6 +200,7 @@ CircleCI
Github Actions / Workflow
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'
- Visual Tests. Triggered with Github Label Event 'pr:visual'
#### 3. Scheduled / Batch Testing
@ -351,6 +352,10 @@ When looking at the reports run in CI, you'll leverage this same HTML Report whi
### e2e Code Coverage
Our e2e code coverage is captured and combined with our unit test coverage. For more information, please see our [code coverage documentation](../TESTING.md)
#### Generating e2e code coverage
Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
```npm run cov:e2e:report```
@ -361,10 +366,6 @@ At this point, the nyc linecov report can be published to [codecov.io](https://a
or
```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
## Other
### About e2e testing

View File

@ -74,7 +74,6 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle');
//Click the Create button
await page.click('button:has-text("Create")');
@ -140,6 +139,7 @@ async function createNotification(page, createNotificationOptions) {
}
/**
* Expand an item in the tree by a given object name.
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
@ -272,6 +272,7 @@ async function getFocusedObjectUuid(page) {
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
await page.waitForLoadState('load'); //Add some determinism
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url = './#/browse/' + [...path].reverse()

View File

@ -170,5 +170,6 @@ exports.test = base.test.extend({
}
}
});
exports.expect = expect;
exports.waitForAnimations = waitForAnimations;

View File

@ -58,8 +58,14 @@ async function navigateToFaultManagementWithoutExample(page) {
async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
// Click text=Fault Management
await page.click('text=Fault Management'); // this verifies the plugin has been added
const faultManagementTreeItem = page.getByRole('tree', {
name: "Main Tree"
}).getByRole('treeitem', {
name: "Fault Management"
});
// Navigate to "Fault Management" from the tree
await faultManagementTreeItem.click();
}
/**
@ -141,8 +147,7 @@ async function clearSearch(page) {
* @param {import('@playwright/test').Page} page
*/
async function selectFaultItem(page, rowNumber) {
// eslint-disable-next-line playwright/no-force-option
await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
}
/**

View File

@ -28,7 +28,7 @@ const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page, text) {
// Click .c-notebook__drag-area
// Click the 'Add Notebook Entry' area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
@ -58,6 +58,7 @@ async function dragAndDropEmbed(page, notebookObject) {
* @param {import('@playwright/test').Page} page
*/
async function commitEntry(page) {
//Click the Commit Entry button
await page.locator('.c-ne__save-button > button').click();
}

View File

@ -9,7 +9,7 @@ const NUM_WORKERS = 2;
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
@ -74,7 +74,8 @@ const config = {
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}],
['junit', { outputFile: '../test-results/results.xml' }],
['github']
['github'],
['@deploysentinel/playwright']
]
};

View File

@ -150,3 +150,17 @@ exports.test = test.extend({
}
});
exports.expect = expect;
/**
* Takes a readable stream and returns a string.
* @param {ReadableStream} readable - the readable stream
* @return {Promise<String>} the stringified stream
*/
exports.streamToString = async function (readable) {
let result = '';
for await (const chunk of readable) {
result += chunk;
}
return result;
};

View File

@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, createNotification, expandEntireTree } =
test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const e2eFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
@ -86,7 +86,7 @@ test.describe('AppActions', () => {
});
});
test("createNotification", async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await createNotification(page, {
message: 'Test info notification',
severity: 'info'
@ -110,7 +110,7 @@ test.describe('AppActions', () => {
await page.locator('[aria-label="Dismiss"]').click();
});
test('expandEntireTree', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const rootFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder'

View File

@ -32,7 +32,7 @@ test.describe('baseFixtures tests', () => {
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail();
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([
@ -43,7 +43,7 @@ test.describe('baseFixtures tests', () => {
});
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([

View File

@ -63,7 +63,7 @@ test.describe('Renaming Timer Object', () => {
let timer;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.
// This example will create a Timer object with default properties, under the root folder:

View File

@ -36,7 +36,7 @@ const { test, expect } = require('../../pluginFixtures.js');
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
// click create button

View File

@ -30,7 +30,7 @@ test.describe('recycled_local_storage @localStorage', () => {
//We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
test('Can use recycled_local_storage file', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
});

View File

@ -29,7 +29,7 @@ const { test, expect } = require('../../baseFixtures.js');
test.describe('Branding tests', () => {
test('About Modal launches with basic branding properties', async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Click About button
await page.click('.l-shell__app-logo');
@ -47,7 +47,7 @@ test.describe('Branding tests', () => {
});
test('Verify Links in About Modal @2p', async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Click About button
await page.click('.l-shell__app-logo');

View File

@ -100,7 +100,7 @@ test.describe("CouchDB initialization with mocked responses @couchdb", () => {
&& req.method() === 'GET');
// Go to baseURL.
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Wait for both requests to resolve.
await Promise.all([

View File

@ -30,7 +30,7 @@ const { createDomainObjectWithDefaults } = require('../../../appActions');
test.describe('Example Event Generator CRUD Operations', () => {
test('Can create a Test Event Generator and it results in the table View', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Create a name for the object
const newObjectName = 'Test Event Generator';

View File

@ -32,7 +32,7 @@ test.describe('Sine Wave Generator', () => {
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@ -36,7 +36,7 @@ const imageFilePath = 'e2e/test-data/rick.jpg';
test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.click('button:has-text("Create")');
await page.click(':nth-match(:text("Folder"), 2)');
@ -77,7 +77,7 @@ test.describe('Form File Input Behavior', () => {
});
test('Can select a JSON file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();
@ -91,7 +91,7 @@ test.describe('Form File Input Behavior', () => {
});
test('Can select an image file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();
@ -117,7 +117,7 @@ test.describe('Persistence operations @addInit', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4323'
});
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.click('button:has-text("Create")');
@ -138,7 +138,7 @@ test.describe('Persistence operations @couchdb', () => {
description: 'https://github.com/nasa/openmct/issues/5616'
});
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
@ -166,12 +166,13 @@ test.describe('Persistence operations @couchdb', () => {
timeout: 1000
}).toEqual(1);
});
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
test('Can create an object after a conflict error @couchdb @2p', async ({ page, openmctConfig }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5982'
});
const { myItemsFolderName } = openmctConfig;
// Instantiate a second page/tab
const page2 = await page.context().newPage();
// Both pages: Go to baseURL
@ -180,6 +181,10 @@ test.describe('Persistence operations @couchdb', () => {
page2.goto('./', { waitUntil: 'networkidle' })
]);
//Slow down the test a bit
await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible();
await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible();
// Both pages: Click the Create button
await Promise.all([
page.click('button:has-text("Create")'),

View File

@ -36,7 +36,7 @@ test.describe('Persistence operations @addInit', () => {
});
test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('text=Persistence Testing').first().click({
button: 'right'

View File

@ -35,7 +35,7 @@ test.describe('Notifications List', () => {
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create an error notification with the message "Error message"
await createNotification(page, {
@ -80,7 +80,7 @@ test.describe('Notification Overlay', () => {
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });

View File

@ -29,7 +29,7 @@ const { getPreciseDuration } = require('../../../../src/utils/duration');
test.describe("Gantt Chart", () => {
let ganttChart;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
ganttChart = await createDomainObjectWithDefaults(page, {
type: 'Gantt Chart'
});

View File

@ -27,7 +27,7 @@ const { assertPlanActivities } = require('../../../helper/planningUtils');
test.describe("Plan", () => {
let plan;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
plan = await createPlanFromJSON(page, {
json: testPlan1
});

View File

@ -80,7 +80,7 @@ test.describe("Time Strip", () => {
const activityBounds = page.locator('.activity-bounds');
// Goto baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timestrip = await test.step("Create a Time Strip", async () => {
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });

View File

@ -34,7 +34,7 @@ test.describe('Clock Generator CRUD Operations', () => {
description: 'https://github.com/nasa/openmct/issues/4878'
});
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@ -37,7 +37,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//TODO: This needs to be refactored
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.click('button:has-text("Create")');
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
@ -148,7 +148,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
@ -182,7 +182,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test.describe('Basic Condition Set Use', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can add a condition', async ({ page }) => {
// Create a new condition set
@ -247,7 +247,7 @@ test.describe('Basic Condition Set Use', () => {
});
test('ConditionSet should output blank instead of the default value', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@ -27,7 +27,7 @@ test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page);
// Create Sine Wave Generator

View File

@ -22,6 +22,7 @@
const { test, expect } = require('../../../../pluginFixtures');
const utils = require('../../../../helper/faultUtils');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => {
@ -38,6 +39,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ page }) => {
await utils.selectFaultItem(page, 1);
await selectInspectorTab(page, 'Fault Management Configuration');
const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent();
const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count();
@ -52,6 +54,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
expect.soft(await selectedRows.count()).toEqual(2);
await selectInspectorTab(page, 'Fault Management Configuration');
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count();

View File

@ -27,7 +27,7 @@ test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {

View File

@ -31,7 +31,7 @@ const uuid = require('uuid').v4;
test.describe('Gauge', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {

View File

@ -37,7 +37,7 @@ const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
test.describe('Example Imagery Object', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
@ -178,7 +178,7 @@ test.describe('Example Imagery in Display Layout', () => {
let displayLayout;
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
await page.goto(displayLayout.url);
@ -317,7 +317,7 @@ test.describe('Example Imagery in Display Layout', () => {
test.describe('Example Imagery in Flexible layout', () => {
let flexibleLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
await page.goto(flexibleLayout.url);
@ -359,7 +359,7 @@ test.describe('Example Imagery in Flexible layout', () => {
test.describe('Example Imagery in Tabs View', () => {
let tabsView;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
await page.goto(tabsView.url);
@ -395,7 +395,7 @@ test.describe('Example Imagery in Tabs View', () => {
test.describe('Example Imagery in Time Strip', () => {
let timeStripObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
timeStripObject = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});

View File

@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRea
test.describe('Testing LAD table configuration', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create LAD table
const ladTable = await createDomainObjectWithDefaults(page, {
@ -139,7 +139,7 @@ test.describe('Testing LAD table configuration', () => {
test.describe('Testing LAD table @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page);
// Create Sine Wave Generator

View File

@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect, streamToString } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');
const path = require('path');
@ -72,7 +72,7 @@ test.describe('Notebook section tests', () => {
//The following test cases are associated with Notebook Sections
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
@ -133,7 +133,7 @@ test.describe('Notebook page tests', () => {
//The following test cases are associated with Notebook Pages
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
@ -198,6 +198,36 @@ test.describe('Notebook page tests', () => {
});
});
test.describe('Notebook export tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('can export notebook as text', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
expect(exportedText).toContain('Foo bar entry');
});
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
});
test.describe('Notebook search tests', () => {
test.fixme('Can search for a single result', async ({ page }) => {});
test.fixme('Can search for many results', async ({ page }) => {});
@ -213,16 +243,24 @@ test.describe('Notebook entry tests', () => {
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') });
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
notebookObject = await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
test('When a new entry is created, it should be focused and selected', async ({ page }) => {
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Click .c-notebook__drag-area
await page.locator('.c-notebook__drag-area').click();
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/);
});
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
@ -232,17 +270,17 @@ test.describe('Notebook entry tests', () => {
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, '.c-notebook__drag-area');
const embed = page.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
expect(embedName).toBe(overlayPlot.name);
});
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
@ -253,17 +291,35 @@ test.describe('Notebook entry tests', () => {
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, 'text=Entry to drop into');
const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') });
const embed = existingEntry.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
expect(embedName).toBe(overlayPlot.name);
});
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
test('previous and new entries can be deleted', async ({ page }) => {
// Navigate to the notebook object
await page.goto(notebookObject.url);
await nbUtils.enterTextEntry(page, 'First Entry');
await page.hover('text="First Entry"');
await page.click('button[title="Delete this entry"]');
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
await expect(page.locator('text="First Entry"')).toBeHidden();
await nbUtils.enterTextEntry(page, 'Another First Entry');
await nbUtils.enterTextEntry(page, 'Second Entry');
await nbUtils.enterTextEntry(page, 'Third Entry');
await page.hover('[aria-label="Notebook Entry"] >> nth=2');
await page.click('button[title="Delete this entry"] >> nth=2');
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
await expect(page.locator('text="Third Entry"')).toBeHidden();
await expect(page.locator('text="Another First Entry"')).toBeVisible();
await expect(page.locator('text="Second Entry"')).toBeVisible();
});
test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const TEST_LINK = 'http://www.google.com';

View File

@ -66,7 +66,7 @@ test.describe('Snapshot Menu tests', () => {
test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {

View File

@ -26,46 +26,49 @@ This test suite is dedicated to tests which verify the basic operations surround
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');
test.describe('Notebook Tests with CouchDB @couchdb', () => {
let testNotebook;
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "TestNotebook"
});
testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' });
await page.goto(testNotebook.url, { waitUntil: 'networkidle'});
});
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();
// Collect all request events to count and assert after notebook action
let addingNotebookElementsRequests = [];
page.on('request', (request) => addingNotebookElementsRequests.push(request));
let notebookElementsRequests = [];
page.on('request', (request) => notebookElementsRequests.push(request));
//Clicking Add Page generates
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.click('[aria-label="Add Page"]'),
// Ensures that there are no other network requests
page.waitForLoadState('networkidle')
page.click('[aria-label="Add Page"]')
]);
// Ensures that there are no other network requests
await page.waitForLoadState('networkidle');
// Assert that only two requests are made
// Network Requests are:
// 1) The actual POST to create the page
// 2) The shared worker event from 👆 request
expect(addingNotebookElementsRequests.length).toBe(2);
expect(notebookElementsRequests.length).toBe(2);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
@ -73,13 +76,10 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// Network Requests are:
// 1) The actual POST to create the entry
// 2) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'First Entry');
await page.waitForLoadState('networkidle');
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags
// Network Requests are for each tag creation are:
@ -95,32 +95,17 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 10) Entry is timestamped
// 11) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
notebookElementsRequests = [];
await addTagAndAwaitNetwork(page, 'Driving');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
notebookElementsRequests = [];
await addTagAndAwaitNetwork(page, 'Drilling');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
notebookElementsRequests = [];
await addTagAndAwaitNetwork(page, 'Science');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
// Delete all the tags
// Network requests are:
@ -129,58 +114,25 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
// This happens for 3 tags so 12 requests
addingNotebookElementsRequests = [];
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
await page.locator('[aria-label="Remove tag Drilling"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
page.hover('[aria-label="Tag"]:has-text("Science")');
await page.locator('[aria-label="Remove tag Science"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
notebookElementsRequests = [];
await removeTagAndAwaitNetwork(page, 'Driving');
await removeTagAndAwaitNetwork(page, 'Drilling');
await removeTagAndAwaitNetwork(page, 'Science');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12);
// Add two more pages
await page.click('[aria-label="Add Page"]');
await page.click('[aria-label="Add Page"]');
// Add three entries
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
await nbUtils.enterTextEntry(page, 'First Entry');
await nbUtils.enterTextEntry(page, 'Second Entry');
await nbUtils.enterTextEntry(page, 'Third Entry');
// Add three tags
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
await addTagAndAwaitNetwork(page, 'Science');
await addTagAndAwaitNetwork(page, 'Drilling');
await addTagAndAwaitNetwork(page, 'Driving');
// Add a fourth entry
// Network requests are:
@ -188,14 +140,11 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fourth Entry');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a fifth entry
// Network requests are:
@ -203,28 +152,22 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fifth Entry');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a sixth entry
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Sixth Entry');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
});
test('Search tests', async ({ page }) => {
@ -233,35 +176,21 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await page.getByText('Annotations').click();
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await nbUtils.enterTextEntry(page, 'First Entry');
// Add three tags
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
await addTagAndAwaitNetwork(page, 'Science');
await addTagAndAwaitNetwork(page, 'Drilling');
await addTagAndAwaitNetwork(page, 'Driving');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
//Partial match for "Science" should only return Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Drilling");
//Searching for a tag which does not exist should return an empty result
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
@ -275,3 +204,40 @@ function filterNonFetchRequests(requests) {
return (request.resourceType() === 'fetch');
});
}
/**
* Add a tag to a notebook entry by providing a tagName.
* Reduces indeterminism by waiting until all necessary requests are completed.
* @param {import('@playwright/test').Page} page
* @param {string} tagName
*/
async function addTagAndAwaitNetwork(page, tagName) {
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(),
expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible()
]);
await page.waitForLoadState('networkidle');
}
/**
* Remove a tag to a notebook entry by providing a tagName.
* Reduces indeterminism by waiting until all necessary requests are completed.
* @param {import('@playwright/test').Page} page
* @param {string} tagName
*/
async function removeTagAndAwaitNetwork(page, tagName) {
await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`);
await Promise.all([
page.locator(`[aria-label="Remove tag ${tagName}"]`).click(),
//With this pattern, we're awaiting the response but asserting on the request payload.
page.waitForResponse(resp => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201)
]);
await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden();
await page.waitForLoadState('networkidle');
}

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect, streamToString } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
const nbUtils = require('../../../../helper/notebookUtils');
@ -169,13 +169,40 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
});
test.describe('can export restricted notebook as text', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
});
test('basic functionality ', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
expect(exportedText).toContain('Foo bar entry');
});
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}

View File

@ -21,7 +21,7 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which verify form functionality.
This test suite is dedicated to tests which verify notebook tag functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
@ -34,9 +34,6 @@ const nbUtils = require('../../../../helper/notebookUtils');
* @param {number} [iterations = 1] - the number of entries to create
*/
async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
@ -81,12 +78,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
}
test.describe('Tagging in Notebooks @addInit', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations');
await page.locator('button:has-text("Add Tag")').click();
@ -110,12 +108,24 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
});
test('Can add tags with blank entry', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await selectInspectorTab(page, 'Annotations');
await nbUtils.enterTextEntry(page, '');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
});
test('Can cancel adding tags', async ({ page }) => {
await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations');
// Test canceling adding a tag after we click "Type to select tag"
@ -204,7 +214,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('button[title="More options"]').click();
await page.locator('li[title="Remove this object from its containing object."]').click();
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible();
@ -215,37 +225,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const ITERATIONS = 4;
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
await page.goto(notebook.url);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
await Promise.all([
page.waitForNavigation(),
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
// Click Clock
await treePane.getByRole('treeitem', {
name: clock.name
}).click();
// Click Notebook
await page.getByRole('treeitem', {
name: notebook.name
}).click();
// Verify tags are present
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
@ -253,14 +239,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
}
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
// Click Notebook
await page.click(`text="${notebook.name}"`);
await page.reload({ waitUntil: 'domcontentloaded' });
// Verify tags persist across reload
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
@ -270,9 +251,6 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can cancel adding a tag', async ({ page }) => {
await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations');
// Click on the "Add Tag" button

View File

@ -44,7 +44,7 @@ test.describe('Operator Status', () => {
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
// verify that operator status is visible

View File

@ -40,7 +40,7 @@ test.describe('Autoscale', () => {
//This is necessary due to the size of the test suite.
test.slow();
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setTimeRange(page);
@ -73,6 +73,7 @@ test.describe('Autoscale', () => {
const canvas = page.locator('canvas').nth(1);
await canvas.hover({trial: true});
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
@ -172,7 +173,7 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
*/
async function turnOffAutoscale(page) {
// uncheck autoscale
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
}
/**
@ -182,14 +183,9 @@ async function turnOffAutoscale(page) {
*/
async function setUserDefinedMinAndMax(page, min, max) {
// set minimum value
const minRangeInput = page.getByRole('listitem').filter({ hasText: 'Minimum Value' }).locator('input[type="number"]');
await minRangeInput.click();
await minRangeInput.fill(min);
await page.getByRole('spinbutton').first().fill(min);
// set maximum value
const maxRangeInput = page.getByRole('listitem').filter({ hasText: 'Maximum Value' }).locator('input[type="number"]');
await maxRangeInput.click();
await maxRangeInput.fill(max);
await page.getByRole('spinbutton').nth(1).fill(max);
}
/**

View File

@ -76,7 +76,7 @@ test.describe('Log plot tests', () => {
*/
async function makeOverlayPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
@ -147,7 +147,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
* @param {import('@playwright/test').Page} page
*/
async function testRegularTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
const yTicks = page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(7);
await expect(yTicks.nth(0)).toHaveText('-2');
await expect(yTicks.nth(1)).toHaveText('0');
@ -162,7 +162,7 @@ async function testRegularTicks(page) {
* @param {import('@playwright/test').Page} page
*/
async function testLogTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
const yTicks = page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(9);
await expect(yTicks.nth(0)).toHaveText('-2.98');
await expect(yTicks.nth(1)).toHaveText('-1.51');
@ -180,27 +180,24 @@ async function testLogTicks(page) {
*/
async function enableEditMode(page) {
// turn on edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
await page.getByRole('button', { name: 'Edit' }).click();
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableLogMode(page) {
// turn on log mode
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).not.toBeChecked();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked();
await page.getByRole('checkbox', { name: 'Log mode' }).check();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function disableLogMode(page) {
// turn off log mode
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).toBeChecked();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked();
await page.getByRole('checkbox', { name: 'Log mode' }).uncheck();
}
/**

View File

@ -84,7 +84,7 @@ test.describe('Handle missing object for plots', () => {
*/
async function makeStackedPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// create stacked plot
await page.locator('button.c-create-button').click();

View File

@ -30,7 +30,7 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../..
test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
@ -214,12 +214,17 @@ test.describe('Overlay Plot', () => {
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
// Wait for "View Large" plot series data to load and be drawn
await expect(page.locator('.c-overlay .js-series-data-loaded')).toBeVisible();
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});

View File

@ -33,7 +33,7 @@ test.describe('Plot Integrity Testing @unstable', () => {
test.beforeEach(async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
});

View File

@ -33,7 +33,7 @@ test.describe('Scatter Plot', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });

View File

@ -36,7 +36,7 @@ test.describe('Stacked Plot', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
stackedPlot = await createDomainObjectWithDefaults(page, {
type: "Stacked Plot"
@ -138,7 +138,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: "Y Axis" })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
// Click on the 2nd plot
@ -146,7 +146,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
// Click on the 3rd plot
@ -154,7 +154,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
// Go into edit mode
@ -167,7 +167,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
//Click on canvas for the 2nd plot
@ -175,7 +175,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
//Click on canvas for the 3rd plot
@ -183,7 +183,7 @@ test.describe('Stacked Plot', () => {
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
});
});

View File

@ -157,7 +157,7 @@ test.describe('Plot Tagging', () => {
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Tags work with Overlay Plots', async ({ page }) => {

View File

@ -30,7 +30,7 @@ test.describe('Telemetry Table', () => {
description: 'https://github.com/nasa/openmct/issues/5113'
});
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {

View File

@ -26,7 +26,7 @@ const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = requ
test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
const year = new Date().getFullYear();
let startDate = 'xxxx-01-01 01:00:00.000Z';
@ -82,7 +82,7 @@ test.describe('Time conductor input fields real-time mode', () => {
};
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Switch to real-time mode
await setRealTimeMode(page);
@ -119,7 +119,7 @@ test.describe('Time conductor input fields real-time mode', () => {
const endDelta = (1 * 1000);
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Switch to real-time mode
await setRealTimeMode(page);

View File

@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('.
test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});

View File

@ -32,7 +32,7 @@ test.describe('Recent Objects', () => {
/** @type {import('@playwright/test').Locator} */
let folderA;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Set Recent Objects List locator for subsequent tests
recentObjectsList = page.getByRole('list', {
@ -58,7 +58,7 @@ test.describe('Recent Objects', () => {
});
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => {
// Verify that both created objects appear in the list and are in the correct order
assertInitialRecentObjectsListState();
await assertInitialRecentObjectsListState();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
@ -149,9 +149,9 @@ test.describe('Recent Objects', () => {
await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
});
test("Persists on refresh", async ({ page }) => {
assertInitialRecentObjectsListState();
await assertInitialRecentObjectsListState();
await page.reload();
assertInitialRecentObjectsListState();
await assertInitialRecentObjectsListState();
});
test("Displays objects and aliases uniquely", async ({ page }) => {
const mainTree = page.getByRole('tree', { name: 'Main Tree'});
@ -252,13 +252,57 @@ test.describe('Recent Objects', () => {
// Assert that the list is empty
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
});
test("Ensure clear recent objects button is active or inactive", async ({ page }) => {
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
// Assert that the button is enabled
expect(
await page
.getByRole("button", { name: "Clear Recently Viewed" })
.isEnabled()
).toBe(true);
// Click the aria-label="Clear Recently Viewed" button
await page.getByRole("button", { name: "Clear Recently Viewed" }).click();
// Click on the "OK" button in the confirmation dialog
await page.getByRole("button", { name: "OK" }).click();
// Assert that the list is empty
expect(
await recentObjectsList.locator(".c-recentobjects-listitem").count()
).toBe(0);
// Assert that the button is disabled
expect(
await page
.getByRole("button", { name: "Clear Recently Viewed" })
.isEnabled()
).toBe(false);
// Navigate to folder object
await page.goto(folderA.url);
// Assert that the list contains 1 object
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1);
// Assert that the button is enabled
expect(
await page
.getByRole("button", { name: "Clear Recently Viewed" })
.isEnabled()
).toBe(true);
});
function assertInitialRecentObjectsListState() {
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
return Promise.all([
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(),
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(),
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(),
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(),
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(),
expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible()
]);
}
});

View File

@ -38,7 +38,7 @@ const { test, expect } = require('../../pluginFixtures');
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@ -28,7 +28,7 @@ const {
test.describe('Main Tree', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ page }) => {

View File

@ -34,7 +34,7 @@ test.describe('Visual - Check Notification Info Banner of \'Save successful\'',
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page }) => {
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page, theme }) => {
// Create a clock domain object
await createDomainObjectWithDefaults(page, { type: 'Clock' });
// Verify there is a button with aria-label="Review 1 Notification"
@ -47,7 +47,7 @@ test.describe('Visual - Check Notification Info Banner of \'Save successful\'',
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Verify the div with role="dialog" contains text "Save successful"
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
await percySnapshot(page, 'Notification banner');
await percySnapshot(page, `Notification banner - ${theme}`);
// Verify there is a button with text "Dismiss"
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
// Click on button with text "Dismiss"

View File

@ -24,7 +24,9 @@ const { test } = require('../../pluginFixtures');
const { setBoundsToSpanAllActivities } = require('../../helper/planningUtils');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions');
const percySnapshot = require('@percy/playwright');
const examplePlanLarge = require('../../test-data/examplePlans/ExamplePlan_Large.json');
const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json');
const snapshotScope = '.c-object-view';
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
@ -32,21 +34,25 @@ test.describe('Visual - Planning', () => {
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
json: examplePlanLarge
json: examplePlanSmall
});
await setBoundsToSpanAllActivities(page, examplePlanLarge, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`, {
scope: snapshotScope
});
});
test('Gantt Chart View', async ({ page, theme }) => {
const ganttChart = await createDomainObjectWithDefaults(page, {
type: 'Gantt Chart'
});
await createPlanFromJSON(page, {
json: examplePlanLarge,
json: examplePlanSmall,
parent: ganttChart.uuid
});
await setBoundsToSpanAllActivities(page, examplePlanLarge, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
scope: snapshotScope
});
});
});

View File

@ -33,6 +33,8 @@ export default function (staticFaults = false) {
return Promise.resolve(faultsData);
},
subscribe(domainObject, callback) {
callback({ type: 'global-alarm-status' });
return () => {};
},
supportsRequest(domainObject) {

View File

@ -200,6 +200,8 @@
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.start();
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
</script>
</html>

View File

@ -1,13 +1,14 @@
{
"name": "openmct",
"version": "2.2.1-SNAPSHOT",
"version": "2.2.3-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.17.0",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.23.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@playwright/test": "1.32.3",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.192",
@ -20,10 +21,10 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.36.0",
"eslint-plugin-compat": "4.1.1",
"eslint": "8.37.0",
"eslint-plugin-compat": "4.1.4",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-vue": "9.10.0",
"eslint-plugin-vue": "9.11.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
@ -38,10 +39,10 @@
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"kdbush": "^3.0.0",
"kdbush": "3.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.5",
@ -50,29 +51,29 @@
"moment-timezone": "0.5.41",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.29.0",
"playwright-core": "1.32.3",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.10.0",
"sass": "1.59.3",
"sass-loader": "13.2.1",
"sass": "1.62.0",
"sass-loader": "13.2.2",
"sinon": "15.0.1",
"style-loader": "^3.3.1",
"typescript": "4.9.5",
"style-loader": "3.3.2",
"typescript": "5.0.4",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.76.3",
"webpack": "5.79.0",
"webpack-cli": "5.0.0",
"webpack-dev-server": "4.11.1",
"webpack-dev-server": "4.13.3",
"webpack-merge": "5.8.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
@ -86,7 +87,7 @@
"test": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",
"test:e2e": "npx playwright test",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
@ -107,14 +108,15 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=14.19.1"
"node": ">=16.19.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",
"last 2 Chrome versions",
"unreleased Chrome versions",
"ios_saf > 15"
"ios_saf >= 16",
"Safari >= 16"
],
"author": "",
"license": "Apache-2.0"

View File

@ -31,7 +31,7 @@ class ActionsAPI extends EventEmitter {
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];
this.register = this.register.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);

View File

@ -21,18 +21,31 @@
*****************************************************************************/
export default class FaultManagementAPI {
/**
* @param {import("openmct").OpenMCT} openmct
*/
constructor(openmct) {
this.openmct = openmct;
}
/**
* @param {*} provider
*/
addProvider(provider) {
this.provider = provider;
}
/**
* @returns {boolean}
*/
supportsActions() {
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @returns {Promise.<FaultAPIResponse[]>}
*/
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
@ -41,6 +54,11 @@ export default class FaultManagementAPI {
return this.provider.request(domainObject);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @param {Function} callback
* @returns {Function} unsubscribe
*/
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
@ -49,58 +67,55 @@ export default class FaultManagementAPI {
return this.provider.subscribe(domainObject, callback);
}
/**
* @param {Fault} fault
* @param {*} ackData
*/
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
/**
* @param {Fault} fault
* @param {*} shelveData
* @returns {Promise.<T>}
*/
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
}
/** @typedef {object} Fault
* @property {string} type
* @property {object} fault
* @property {boolean} fault.acknowledged
* @property {object} fault.currentValueInfo
* @property {number} fault.currentValueInfo.value
* @property {string} fault.currentValueInfo.rangeCondition
* @property {string} fault.currentValueInfo.monitoringResult
* @property {string} fault.id
* @property {string} fault.name
* @property {string} fault.namespace
* @property {number} fault.seqNum
* @property {string} fault.severity
* @property {boolean} fault.shelved
* @property {string} fault.shortDescription
* @property {string} fault.triggerTime
* @property {object} fault.triggerValueInfo
* @property {number} fault.triggerValueInfo.value
* @property {string} fault.triggerValueInfo.rangeCondition
* @property {string} fault.triggerValueInfo.monitoringResult
* @example
* {
* "type": "",
* "fault": {
* "acknowledged": true,
* "currentValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* },
* "id": "",
* "name": "",
* "namespace": "",
* "seqNum": 0,
* "severity": "",
* "shelved": true,
* "shortDescription": "",
* "triggerTime": "",
* "triggerValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* }
* }
* }
/**
* @typedef {object} TriggerValueInfo
* @property {number} value
* @property {string} rangeCondition
* @property {string} monitoringResult
*/
/**
* @typedef {object} CurrentValueInfo
* @property {number} value
* @property {string} rangeCondition
* @property {string} monitoringResult
*/
/**
* @typedef {object} Fault
* @property {boolean} acknowledged
* @property {CurrentValueInfo} currentValueInfo
* @property {string} id
* @property {string} name
* @property {string} namespace
* @property {number} seqNum
* @property {string} severity
* @property {boolean} shelved
* @property {string} shortDescription
* @property {string} triggerTime
* @property {TriggerValueInfo} triggerValueInfo
*/
/**
* @typedef {object} FaultAPIResponse
* @property {string} type
* @property {Fault} fault
*/

View File

@ -20,13 +20,15 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
import TimeContext from "./TimeContext";
import { TIME_CONTEXT_EVENTS } from './constants';
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(openmct, globalTimeContext, objectPath) {
super();
this.openmct = openmct;
@ -46,7 +48,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds(newBounds) {
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
@ -54,7 +56,23 @@ class IndependentTimeContext extends TimeContext {
}
}
tick(timestamp) {
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
} else {
return super.getBounds();
}
}
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
} else {
return super.setBounds(...arguments);
}
}
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
@ -62,7 +80,7 @@ class IndependentTimeContext extends TimeContext {
}
}
clockOffsets(offsets) {
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
@ -70,11 +88,27 @@ class IndependentTimeContext extends TimeContext {
}
}
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
} else {
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
}
}
stopClock() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
// this.upstreamTimeContext.stopClock();
} else {
super.stopClock();
// super.stopClock();
}
}
@ -86,6 +120,16 @@ class IndependentTimeContext extends TimeContext {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
@ -146,6 +190,121 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClock();
}
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClock(...arguments);
}
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getMode();
}
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setMode(...arguments);
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@ -153,7 +312,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
@ -187,6 +346,7 @@ class IndependentTimeContext extends TimeContext {
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
@ -199,6 +359,7 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('boundsChanged', this.getBounds());
}
hasOwnContext() {
@ -236,6 +397,7 @@ class IndependentTimeContext extends TimeContext {
*/
removeIndependentContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
@ -261,7 +423,8 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('bounds', this.getBounds());
this.emit('boundsChanged', this.getBounds());
// 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

@ -22,6 +22,7 @@
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
import {FIXED_MODE_KEY, REALTIME_MODE_KEY} from "@/api/time/constants";
/**
* The public API for setting and querying the temporal state of the
@ -134,14 +135,27 @@ class TimeAPI extends GlobalTimeContext {
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
let upstreamClock;
if (timeContext.upstreamTimeContext) {
upstreamClock = timeContext.upstreamTimeContext.getClock();
}
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
timeContext.setMode(REALTIME_MODE_KEY);
timeContext.setClock(clockKey, value);
} else {
timeContext.setMode(FIXED_MODE_KEY);
//TODO: Should the clock be stopped here?
timeContext.stopClock();
timeContext.bounds(value);
//upstream clock was active, but now we don't have one
if (upstreamClock) {
timeContext.emit('clockChanged', timeContext.activeClock);
}
timeContext.setBounds(value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@ -179,12 +193,14 @@ class TimeAPI extends GlobalTimeContext {
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
if (!viewKey) {
// Return the global time context
// Return the global time contex
return this;
}
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
console.log('no view context for viewKey', viewKey, this.openmct.objects.getRelativePath(objectPath));
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
@ -193,6 +209,8 @@ class TimeAPI extends GlobalTimeContext {
const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
const newPath = this.openmct.objects.getRelativePath(objectPath);
console.log('view context exists, paths match?', !(currentPath !== newPath), currentPath, newPath, 'view key: ', viewKey);
if (currentPath !== newPath) {
// If the path has changed, update the context.
this.independentContexts.delete(viewKey);

View File

@ -21,13 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY } from './constants';
class TimeContext extends EventEmitter {
constructor() {
@ -47,6 +41,7 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this);
}
@ -61,6 +56,8 @@ class TimeContext extends EventEmitter {
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
@ -91,7 +88,7 @@ class TimeContext extends EventEmitter {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
@ -102,7 +99,7 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
this.emit('timeSystem', this.#copy(this.system));
if (bounds) {
this.bounds(bounds);
}
@ -163,6 +160,8 @@ class TimeContext extends EventEmitter {
* @method bounds
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
@ -170,7 +169,7 @@ class TimeContext extends EventEmitter {
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
@ -183,7 +182,7 @@ class TimeContext extends EventEmitter {
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
return this.#copy(this.boundsVal);
}
/**
@ -247,6 +246,8 @@ class TimeContext extends EventEmitter {
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
@ -278,11 +279,14 @@ class TimeContext extends EventEmitter {
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
console.log('stop clock');
this.#warnMethodDeprecated('"stopClock"');
if (this.activeClock) {
this.clock(undefined, undefined);
}
@ -301,6 +305,8 @@ class TimeContext extends EventEmitter {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
if (arguments.length === 2) {
let clock;
@ -354,25 +360,300 @@ class TimeContext extends EventEmitter {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
if (this.mode === REALTIME_MODE_KEY) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
this.boundsVal = newBounds;
// "bounds" will be deprecated in a future release
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
}
/**
* Checks if this time context is in real-time mode or not.
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (!this.isRealTime() && !bounds) {
throw new Error(
'Must specify bounds when changing time system without an active clock.'
);
}
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
if (bounds) {
this.setBounds(bounds);
}
return this.system;
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
/**
* Checks if this time context is in realtime mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
return this.mode === MODES.realtime;
}
/**
* Checks if this time context is in fixed mode or not.
* @returns {boolean} true if this context is in fixed mode, false if not
*/
isFixed() {
return this.mode === MODES.fixed;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
return false;
this.offsets = this.#copy(offsets);
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
return this.offsets;
}
#warnMethodDeprecated(method, newMethod) {
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {
message += ` Please use the ${newMethod} API method(s) instead.`;
}
// TODO: add docs and point to them in warning.
// For more information and migration instructions, visit [link to documentation or migration guide].
console.warn(message);
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
}

22
src/api/time/constants.js Normal file
View File

@ -0,0 +1,22 @@
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const REALTIME_MODE_KEY = 'realtime';
export const FIXED_MODE_KEY = 'fixed';
export const MODES = {
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
};

View File

@ -20,14 +20,15 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const TIME_EVENTS = ['timeSystemChanged', 'modeChanged', 'clockChanged', 'clockOffsetsChanged'];
const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound';
const SEARCH_END_BOUND = 'tc.endBound';
const SEARCH_START_DELTA = 'tc.startDelta';
const SEARCH_END_DELTA = 'tc.endDelta';
const MODE_FIXED = 'fixed';
import { FIXED_MODE_KEY } from "../../api/time/constants";
export default class URLTimeSettingsSynchronizer {
constructor(openmct) {
@ -67,7 +68,7 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
let timeParameters = this.parseParametersFromUrl();
const timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
@ -78,21 +79,18 @@ export default class URLTimeSettingsSynchronizer {
}
parseParametersFromUrl() {
let searchParams = this.openmct.router.getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let bounds = {
const searchParams = this.openmct.router.getAllSearchParams();
const mode = searchParams.get(SEARCH_MODE);
const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
const bounds = {
start: startBound,
end: endBound
};
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let clockOffsets = {
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
const clockOffsets = {
start: 0 - startOffset,
end: endOffset
};
@ -106,30 +104,30 @@ export default class URLTimeSettingsSynchronizer {
}
setTimeApiFromUrl(timeParameters) {
if (timeParameters.mode === 'fixed') {
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(
timeParameters.timeSystem,
timeParameters.bounds
);
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) {
this.openmct.time.bounds(timeParameters.bounds);
const timeSystem = this.openmct.time.getTimeSystem();
if (timeParameters.mode === FIXED_MODE_KEY) {
// should update timesystem
if (timeSystem.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
} else if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {
this.openmct.time.setBounds(timeParameters.bounds);
}
if (this.openmct.time.clock()) {
this.openmct.time.stopClock();
}
this.openmct.time.setMode('fixed');
} else {
if (!this.openmct.time.clock()
|| this.openmct.time.clock().key !== timeParameters.mode) {
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets);
} else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.clockOffsets(timeParameters.clockOffsets);
const clock = this.openmct.time.getClock();
console.log('setting as realtime', clock?.key, timeParameters.mode);
if (clock?.key !== timeParameters.mode) {
this.openmct.time.setClock(timeParameters.mode, timeParameters.clockOffsets);
} else if (!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.setClockOffsets(timeParameters.clockOffsets);
}
if (!this.openmct.time.timeSystem()
|| this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(timeParameters.timeSystem);
this.openmct.time.setMode('realtime');
if (timeSystem?.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem);
}
}
}
@ -141,13 +139,14 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
let searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
const searchParams = this.openmct.router.getAllSearchParams();
const clock = this.openmct.time.getClock();
const mode = this.openmct.time.getMode();
const bounds = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
if (clock === undefined) {
searchParams.set(SEARCH_MODE, MODE_FIXED);
if (mode === FIXED_MODE_KEY) {
searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end);
@ -168,8 +167,9 @@ export default class URLTimeSettingsSynchronizer {
searchParams.delete(SEARCH_END_BOUND);
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.openmct.router.setAllSearchParams(searchParams);
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
// this.openmct.router.setAllSearchParams(searchParams);
this.openmct.router.updateParams(searchParams);
}
areTimeParametersValid(timeParameters) {
@ -178,7 +178,7 @@ export default class URLTimeSettingsSynchronizer {
if (this.isModeValid(timeParameters.mode)
&& this.isTimeSystemValid(timeParameters.timeSystem)) {
if (timeParameters.mode === 'fixed') {
if (timeParameters.mode === FIXED_MODE_KEY) {
isValid = this.areStartAndEndValid(timeParameters.bounds);
} else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
@ -200,8 +200,9 @@ export default class URLTimeSettingsSynchronizer {
isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined;
if (isValid) {
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined;
}
@ -217,7 +218,7 @@ export default class URLTimeSettingsSynchronizer {
}
if (isValid) {
if (mode.toLowerCase() === MODE_FIXED) {
if (mode.toLowerCase() === FIXED_MODE_KEY) {
isValid = true;
} else {
isValid = this.openmct.time.clocks.get(mode) !== undefined;
@ -228,7 +229,7 @@ export default class URLTimeSettingsSynchronizer {
}
areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds.start === secondBounds.start
&& firstBounds.end === secondBounds.end;
return firstBounds?.start === secondBounds.start
&& firstBounds?.end === secondBounds.end;
}
}

View File

@ -103,11 +103,11 @@ export default {
},
followTimeContext() {
this.timeContext.on('bounds', this.refreshData);
this.timeContext.on('boundsChanged', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData);
this.timeContext.off('boundsChanged', this.refreshData);
}
},
addToComposition(telemetryObject) {

View File

@ -32,7 +32,7 @@ export default class ExportAsJSONAction {
this.key = 'export.JSON';
this.description = '';
this.cssClass = "icon-export";
this.group = "json";
this.group = "export";
this.priority = 1;
this.externalIdentifiers = [];
@ -156,11 +156,35 @@ export default class ExportAsJSONAction {
* @private
*/
_rewriteReferences() {
const oldKeyStrings = Object.keys(this.idMap);
let treeString = JSON.stringify(this.tree);
Object.keys(this.idMap).forEach(function (oldId) {
const newId = this.idMap[oldId];
treeString = treeString.split(oldId).join(newId);
}.bind(this));
oldKeyStrings.forEach((oldKeyString) => {
// this will cover keyStrings, identifiers and identifiers created
// by hand that may be structured differently from those created with 'makeKeyString'
const newKeyString = this.idMap[oldKeyString];
const newIdentifier = JSON.stringify(this.openmct.objects.parseKeyString(newKeyString));
const oldIdentifier = this.openmct.objects.parseKeyString(oldKeyString);
const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier);
const oldIdentifierKeyFirst = JSON.stringify({
key: oldIdentifier.key,
namespace: oldIdentifier.namespace
});
// replace keyStrings
treeString = treeString.split(oldKeyString).join(newKeyString);
// check for namespace first identifiers, replace if necessary
if (treeString.includes(oldIdentifierNamespaceFirst)) {
treeString = treeString.split(oldIdentifierNamespaceFirst).join(newIdentifier);
}
// check for key first identifiers, replace if necessary
if (treeString.includes(oldIdentifierKeyFirst)) {
treeString = treeString.split(oldIdentifierKeyFirst).join(newIdentifier);
}
});
this.tree = JSON.parse(treeString);
}
/**
@ -214,7 +238,9 @@ export default class ExportAsJSONAction {
}
}
});
this._decrementCallsAndSave();
if (!childObjectReferenceId) {
this._decrementCallsAndSave();
}
});
} else if (!childObjectReferenceId) {
this._decrementCallsAndSave();

View File

@ -42,8 +42,6 @@ export default {
};
},
mounted() {
this.updateFaultList();
this.unsubscribe = this.openmct.faults
.subscribe(this.domainObject, this.updateFault);
},
@ -68,7 +66,11 @@ export default {
this.openmct.faults
.request(this.domainObject)
.then(faultsData => {
this.faultsList = faultsData.map(fd => fd.fault);
if (faultsData?.length > 0) {
this.faultsList = faultsData.map(fd => fd.fault);
} else {
this.faultsList = [];
}
});
}
}

View File

@ -29,7 +29,7 @@ export default class ImportAsJSONAction {
this.key = 'import.JSON';
this.description = '';
this.cssClass = "icon-import";
this.group = "json";
this.group = "import";
this.priority = 2;
this.openmct = openmct;

View File

@ -0,0 +1,167 @@
import {saveAs} from 'saveAs';
import Moment from 'moment';
import {NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE} from '../notebook-constants';
const UNKNOWN_USER = 'Unknown';
const UNKNOWN_TIME = 'Unknown';
const ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE];
export default class ExportNotebookAsTextAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-export';
this.description = 'Exports notebook contents as a text file';
this.group = "export";
this.key = 'exportNotebookAsText';
this.name = 'Export Notebook as Text';
}
invoke(objectPath) {
this.showForm(objectPath);
}
getTagName(tagId, availableTags) {
const foundTag = availableTags.find(tag => tag.id === tagId);
if (foundTag) {
return foundTag.label;
} else {
return tagId;
}
}
getTagsForEntry(entry, domainObjectKeyString, annotations) {
const foundTags = [];
annotations.forEach(annotation => {
const target = annotation.targets?.[domainObjectKeyString];
if (target?.entryId === entry.id) {
annotation.tags.forEach(tag => {
if (!foundTags.includes(tag)) {
foundTags.push(tag);
}
});
}
});
return foundTags;
}
formatTimeStamp(timestamp) {
if (timestamp) {
return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`;
} else {
return UNKNOWN_TIME;
}
}
appliesTo(objectPath) {
const domainObject = objectPath[0];
return ALLOWED_TYPES.includes(domainObject.type);
}
async onSave(changes, objectPath) {
const availableTags = this.openmct.annotation.getAvailableTags();
const identifier = objectPath[0].identifier;
const domainObject = await this.openmct.objects.get(identifier);
let foundAnnotations = [];
// only load annotations if there are tags
if (availableTags.length) {
foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier);
}
let notebookAsText = `# ${domainObject.name}\n\n`;
if (changes.exportMetaData) {
const createdTimestamp = domainObject.created;
const createdBy = this.getUserName(domainObject.createdBy);
const modifiedBy = this.getUserName(domainObject.modifiedBy);
const modifiedTimestamp = domainObject.modified ?? domainObject.created;
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
}
const notebookSections = domainObject.configuration.sections;
const notebookEntries = domainObject.configuration.entries;
notebookSections.forEach(section => {
notebookAsText += `## ${section.name}\n\n`;
const notebookPages = section.pages;
notebookPages.forEach(page => {
notebookAsText += `### ${page.name}\n\n`;
const notebookPageEntries = notebookEntries[section.id]?.[page.id];
if (!notebookPageEntries) {
// blank page
return;
}
notebookPageEntries.forEach(entry => {
if (changes.exportMetaData) {
const createdTimestamp = entry.createdOn;
const createdBy = this.getUserName(entry.createdBy);
const modifiedBy = this.getUserName(entry.modifiedBy);
const modifiedTimestamp = entry.modified ?? entry.created;
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
}
if (changes.exportTags) {
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations);
const tagNames = tags.map(tag => this.getTagName(tag, availableTags));
if (tagNames) {
notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`;
}
}
notebookAsText += `${entry.text}\n\n`;
});
});
});
const blob = new Blob([notebookAsText], {type: "text/markdown"});
const fileName = domainObject.name + '.md';
saveAs(blob, fileName);
}
getUserName(userId) {
if (userId && userId.length) {
return userId;
}
return UNKNOWN_USER;
}
async showForm(objectPath) {
const formStructure = {
title: "Export Notebook Text",
sections: [
{
rows: [
{
key: "exportMetaData",
control: "toggleSwitch",
name: "Include Metadata (created/modified, etc.)",
required: true,
value: false
},
{
name: "Include Tags",
control: "toggleSwitch",
required: true,
key: 'exportTags',
value: false
}
]
}
]
};
const changes = await this.openmct.forms.showForm(formStructure);
return this.onSave(changes, objectPath);
}
}

View File

@ -125,7 +125,7 @@
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@click="newEntry(null, $event)"
@dragover="dragOver"
@drop.capture="dropCapture"
@drop="dropOnEntry($event)"
@ -193,7 +193,7 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject, selectEntry } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
@ -793,15 +793,29 @@ export default {
return section.id;
},
async newEntry(embed = null) {
async newEntry(embed, event) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
const element = this.$refs.notebookEntries.querySelector(`#${id}`);
const entryAnnotations = this.notebookAnnotations[id] ?? {};
selectEntry({
element,
entryId: id,
domainObject: this.domainObject,
openmct: this.openmct,
notebookAnnotations: entryAnnotations
});
if (event) {
event.stopPropagation();
}
this.filterAndSortEntries();
this.focusEntryId = id;
this.selectedEntryId = id;
this.filterAndSortEntries();
},
orientationChange() {
this.formatSidebar();

View File

@ -32,7 +32,7 @@
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectEntry($event, entry)"
@click="selectAndEmitEntry($event, entry)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete">
@ -164,7 +164,7 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { createNewEmbed, selectEntry } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import sanitizeHtml from 'sanitize-html';
@ -479,37 +479,18 @@ export default {
updateEntryValue($event) {
this.editMode = false;
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
this.timestampAndUpdate();
} else {
this.$emit('cancelEdit');
}
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
this.timestampAndUpdate();
},
selectEntry(event, entry) {
const targetDetails = {};
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
targetDetails[keyString] = {
entryId: entry.id
};
const targetDomainObjects = {};
targetDomainObjects[keyString] = this.domainObject;
this.openmct.selection.select(
[
{
element: event.currentTarget,
context: {
type: 'notebook-entry-selection',
item: this.domainObject,
targetDetails,
targetDomainObjects,
annotations: this.notebookAnnotations,
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
onAnnotationChange: this.timestampAndUpdate
}
}
],
false);
selectAndEmitEntry(event, entry) {
selectEntry({
element: event.currentTarget,
entryId: entry.id,
domainObject: this.domainObject,
openmct: this.openmct,
onAnnotationChange: this.timestampAndUpdate,
notebookAnnotations: this.notebookAnnotations
});
event.stopPropagation();
this.$emit('entry-selection', this.entry);
}

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import NotebookViewProvider from './NotebookViewProvider';
import NotebookType from './NotebookType';
@ -80,6 +81,7 @@ function installBaseNotebookFunctionality(openmct) {
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
openmct.actions.register(new CopyToNotebookAction(openmct));
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
const notebookSnapshotIndicator = new Vue ({
components: {

View File

@ -2,7 +2,7 @@ import objectLink from '../../../ui/mixins/object-link';
import { v4 as uuid } from 'uuid';
async function getUsername(openmct) {
let username = '';
let username = null;
if (openmct.user.hasProvider()) {
const user = await openmct.user.getCurrentUser();
@ -44,6 +44,35 @@ export function addEntryIntoPage(notebookStorage, entries, entry) {
return newEntries;
}
export function selectEntry({
element, entryId, domainObject, openmct,
onAnnotationChange, notebookAnnotations
}) {
const targetDetails = {};
const keyString = openmct.objects.makeKeyString(domainObject.identifier);
targetDetails[keyString] = {
entryId
};
const targetDomainObjects = {};
targetDomainObjects[keyString] = domainObject;
openmct.selection.select(
[
{
element,
context: {
type: 'notebook-entry-selection',
item: domainObject,
targetDetails,
targetDomainObjects,
annotations: notebookAnnotations,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
onAnnotationChange
}
}
],
false);
}
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
if (historicLink.includes('tc.mode=fixed')) {
return historicLink;

View File

@ -288,7 +288,7 @@ export default {
seriesModels: [],
legend: {},
pending: 0,
isRealTime: this.openmct.time.clock() !== undefined,
isRealTime: this.openmct.time.isRealTime(),
loaded: false,
isTimeOutOfSync: false,
isFrozenOnMouseDown: false,
@ -369,7 +369,7 @@ export default {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateMode = this.updateMode.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
@ -533,19 +533,20 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
console.log('time context mctplot', this.timeContext, this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
this.timeContext.on('clock', this.updateRealTime);
this.timeContext.on('bounds', this.updateDisplayBounds);
this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
this.synchronized(true);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("clock", this.updateRealTime);
this.timeContext.off("bounds", this.updateDisplayBounds);
this.timeContext.off("clockChanged", this.updateMode);
this.timeContext.off("boundsChanged", this.updateDisplayBounds);
}
},
getConfig() {
@ -753,8 +754,8 @@ export default {
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
updateMode() {
this.isRealTime = this.timeContext.isRealTime();
},
/**
@ -815,13 +816,13 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isLocalClock = this.timeContext.clock();
const isRealTime = this.timeContext.isRealTime();
if (typeof value !== 'undefined') {
this._synchronized = value;
this.isTimeOutOfSync = value !== true;
const isUnsynced = isLocalClock && !value;
const isUnsynced = isRealTime && !value;
this.setStatus(isUnsynced);
}

View File

@ -86,17 +86,17 @@ export default {
this.xAxis = this.getXAxisFromConfig();
this.loaded = true;
this.setUpXAxisOptions();
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
},
methods: {
isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.clock();
const inRealTimeMode = this.openmct.time.getClock();
return isSinglePlot && !isFrozen && !inRealTimeMode;
},

View File

@ -19,6 +19,7 @@
</li>
<li class="grid-row">
<div
id="log-mode-checkbox"
class="grid-cell label"
title="Enable log mode."
>
@ -29,6 +30,7 @@
<input
v-model="logMode"
class="js-log-mode-input"
aria-labelledby="log-mode-checkbox"
type="checkbox"
@change="updateForm('logMode')"
/>
@ -36,12 +38,14 @@
</li>
<li class="grid-row">
<div
id="autoscale-checkbox"
class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Auto scale</div>
<div class="grid-cell value"><input
v-model="autoscale"
type="checkbox"
aria-labelledby="autoscale-checkbox"
@change="updateForm('autoscale')"
></div>
</li>

View File

@ -46,6 +46,7 @@ export default class RemoteClock extends DefaultClock {
this.timeTelemetryObject = undefined;
this.parseTime = undefined;
this.formatTime = undefined;
this.metadata = undefined;
this.lastTick = 0;
@ -137,6 +138,10 @@ export default class RemoteClock extends DefaultClock {
this.parseTime = (datum) => {
return timeFormatter.parse(datum);
};
this.formatTime = (datum) => {
return timeFormatter.format(datum);
};
}
/**

View File

@ -46,76 +46,96 @@ class StaticModelProvider {
throw new Error(keyString + ' not found in import models.');
}
parseObjectLeaf(objectLeaf, idMap, namespace) {
parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {
Object.keys(objectLeaf).forEach((nodeKey) => {
if (idMap.get(nodeKey)) {
const newIdentifier = objectUtils.makeKeyString({
namespace,
namespace: newRootNamespace,
key: idMap.get(nodeKey)
});
objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] };
delete objectLeaf[nodeKey];
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, namespace);
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace);
} else {
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, namespace);
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace);
}
});
return objectLeaf;
}
parseArrayLeaf(arrayLeaf, idMap, namespace) {
parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) {
return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf(
null, leafValue, idMap, namespace));
null, leafValue, idMap, newRootNamespace, oldRootNamespace));
}
parseBranchedLeaf(branchedLeafValue, idMap, namespace) {
parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) {
if (Array.isArray(branchedLeafValue)) {
return this.parseArrayLeaf(branchedLeafValue, idMap, namespace);
return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
} else {
return this.parseObjectLeaf(branchedLeafValue, idMap, namespace);
return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
}
}
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) {
if (leafValue === null || leafValue === undefined) {
return leafValue;
}
const hasChild = typeof leafValue === 'object';
if (hasChild) {
return this.parseBranchedLeaf(leafValue, idMap, namespace);
return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace);
}
if (leafKey === 'key') {
return idMap.get(leafValue);
} else if (leafKey === 'namespace') {
return namespace;
} else if (leafKey === 'location') {
if (idMap.get(leafValue)) {
const newLocationIdentifier = objectUtils.makeKeyString({
namespace,
key: idMap.get(leafValue)
});
return newLocationIdentifier;
let mappedLeafValue;
if (oldRootNamespace) {
mappedLeafValue = idMap.get(objectUtils.makeKeyString({
namespace: oldRootNamespace,
key: leafValue
}));
} else {
mappedLeafValue = idMap.get(leafValue);
}
return null;
} else if (idMap.get(leafValue)) {
const newIdentifier = objectUtils.makeKeyString({
namespace,
key: idMap.get(leafValue)
return mappedLeafValue ?? leafValue;
} else if (leafKey === 'namespace') {
// Only rewrite the namespace if it matches the old root namespace.
// This is to prevent rewriting namespaces of objects that are not
// children of the root object (e.g.: objects from a telemetry dictionary)
return leafValue === oldRootNamespace
? newRootNamespace
: leafValue;
} else if (leafKey === 'location') {
const mappedLeafValue = idMap.get(leafValue);
if (!mappedLeafValue) {
return null;
}
const newLocationIdentifier = objectUtils.makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
return newIdentifier;
return newLocationIdentifier;
} else {
return leafValue;
const mappedLeafValue = idMap.get(leafValue);
if (mappedLeafValue) {
const newIdentifier = objectUtils.makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
return newIdentifier;
} else {
return leafValue;
}
}
}
rewriteObjectIdentifiers(importData, rootIdentifier) {
const namespace = rootIdentifier.namespace;
const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId);
const { namespace: newRootNamespace } = rootIdentifier;
const idMap = new Map();
const objectTree = importData.openmct;
@ -128,7 +148,7 @@ class StaticModelProvider {
idMap.set(originalId, newId);
});
const newTree = this.parseTreeLeaf(null, objectTree, idMap, namespace);
const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace);
return newTree;
}

View File

@ -20,130 +20,265 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import testStaticData from './static-provider-test.json';
import testStaticDataEmptyNamespace from './test-data/static-provider-test-empty-namespace.json';
import testStaticDataFooNamespace from './test-data/static-provider-test-foo-namespace.json';
import StaticModelProvider from './StaticModelProvider';
describe('StaticModelProvider', function () {
describe('with empty namespace', function () {
let staticProvider;
beforeEach(function () {
const staticData = JSON.parse(JSON.stringify(testStaticData));
staticProvider = new StaticModelProvider(staticData, {
namespace: 'my-import',
key: 'root'
});
});
describe('rootObject', function () {
let rootModel;
let staticProvider;
beforeEach(function () {
rootModel = staticProvider.get({
const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace));
staticProvider = new StaticModelProvider(staticData, {
namespace: 'my-import',
key: 'root'
});
});
it('is located at top level', function () {
expect(rootModel.location).toBe('ROOT');
describe('rootObject', function () {
let rootModel;
beforeEach(function () {
rootModel = staticProvider.get({
namespace: 'my-import',
key: 'root'
});
});
it('is located at top level', function () {
expect(rootModel.location).toBe('ROOT');
});
it('has remapped identifier', function () {
expect(rootModel.identifier).toEqual({
namespace: 'my-import',
key: 'root'
});
});
it('has remapped identifiers in composition', function () {
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '1'
});
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '2'
});
});
});
it('has new-format identifier', function () {
expect(rootModel.identifier).toEqual({
describe('childObjects', function () {
let swg;
let layout;
let fixed;
beforeEach(function () {
swg = staticProvider.get({
namespace: 'my-import',
key: '1'
});
layout = staticProvider.get({
namespace: 'my-import',
key: '2'
});
fixed = staticProvider.get({
namespace: 'my-import',
key: '3'
});
});
it('match expected ordering', function () {
// this is a sanity check to make sure the identifiers map in
// the correct order.
expect(swg.type).toBe('generator');
expect(layout.type).toBe('layout');
expect(fixed.type).toBe('telemetry.fixed');
});
it('have remapped identifiers', function () {
expect(swg.identifier).toEqual({
namespace: 'my-import',
key: '1'
});
expect(layout.identifier).toEqual({
namespace: 'my-import',
key: '2'
});
expect(fixed.identifier).toEqual({
namespace: 'my-import',
key: '3'
});
});
it('have remapped composition', function () {
expect(layout.composition).toContain({
namespace: 'my-import',
key: '1'
});
expect(layout.composition).toContain({
namespace: 'my-import',
key: '3'
});
expect(fixed.composition).toContain({
namespace: 'my-import',
key: '1'
});
});
it('rewrites locations', function () {
expect(swg.location).toBe('my-import:root');
expect(layout.location).toBe('my-import:root');
expect(fixed.location).toBe('my-import:2');
});
it('rewrites matched identifiers in objects', function () {
expect(layout.configuration.layout.panels['my-import:1'])
.toBeDefined();
expect(layout.configuration.layout.panels['my-import:3'])
.toBeDefined();
expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'])
.not.toBeDefined();
expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'])
.not.toBeDefined();
expect(fixed.configuration['fixed-display'].elements[0].id)
.toBe('my-import:1');
});
});
});
describe('with namespace "foo"', function () {
let staticProvider;
beforeEach(function () {
const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace));
staticProvider = new StaticModelProvider(staticData, {
namespace: 'my-import',
key: 'root'
});
});
it('has new-format composition', function () {
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '1'
});
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '2'
});
});
});
describe('rootObject', function () {
let rootModel;
describe('childObjects', function () {
let swg;
let layout;
let fixed;
beforeEach(function () {
rootModel = staticProvider.get({
namespace: 'my-import',
key: 'root'
});
});
beforeEach(function () {
swg = staticProvider.get({
namespace: 'my-import',
key: '1'
it('is located at top level', function () {
expect(rootModel.location).toBe('ROOT');
});
layout = staticProvider.get({
namespace: 'my-import',
key: '2'
it('has remapped identifier', function () {
expect(rootModel.identifier).toEqual({
namespace: 'my-import',
key: 'root'
});
});
fixed = staticProvider.get({
namespace: 'my-import',
key: '3'
it('has remapped composition', function () {
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '1'
});
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '2'
});
});
});
it('match expected ordering', function () {
// this is a sanity check to make sure the identifiers map in
// the correct order.
expect(swg.type).toBe('generator');
expect(layout.type).toBe('layout');
expect(fixed.type).toBe('telemetry.fixed');
});
describe('childObjects', function () {
let clock;
let layout;
let swg;
let folder;
it('have new-style identifiers', function () {
expect(swg.identifier).toEqual({
namespace: 'my-import',
key: '1'
beforeEach(function () {
folder = staticProvider.get({
namespace: 'my-import',
key: 'root'
});
layout = staticProvider.get({
namespace: 'my-import',
key: '1'
});
swg = staticProvider.get({
namespace: 'my-import',
key: '2'
});
clock = staticProvider.get({
namespace: 'my-import',
key: '3'
});
});
expect(layout.identifier).toEqual({
namespace: 'my-import',
key: '2'
it('match expected ordering', function () {
// this is a sanity check to make sure the identifiers map in
// the correct order.
expect(folder.type).toBe('folder');
expect(swg.type).toBe('generator');
expect(layout.type).toBe('layout');
expect(clock.type).toBe('clock');
});
expect(fixed.identifier).toEqual({
namespace: 'my-import',
key: '3'
it('have remapped identifiers', function () {
expect(folder.identifier).toEqual({
namespace: 'my-import',
key: 'root'
});
expect(layout.identifier).toEqual({
namespace: 'my-import',
key: '1'
});
expect(swg.identifier).toEqual({
namespace: 'my-import',
key: '2'
});
expect(clock.identifier).toEqual({
namespace: 'my-import',
key: '3'
});
});
it('have remapped identifiers in composition', function () {
expect(layout.composition).toContain({
namespace: 'my-import',
key: '2'
});
expect(layout.composition).toContain({
namespace: 'my-import',
key: '3'
});
});
it('layout has remapped identifiers in configuration', function () {
const identifiers = layout.configuration.items
.map(item => item.identifier)
.filter(identifier => identifier !== undefined);
expect(identifiers).toContain({
namespace: 'my-import',
key: '2'
});
expect(identifiers).toContain({
namespace: 'my-import',
key: '3'
});
});
it('rewrites locations', function () {
expect(folder.location).toBe('ROOT');
expect(swg.location).toBe('my-import:root');
expect(layout.location).toBe('my-import:root');
expect(clock.location).toBe('my-import:root');
});
});
it('have new-style composition', function () {
expect(layout.composition).toContain({
namespace: 'my-import',
key: '1'
});
expect(layout.composition).toContain({
namespace: 'my-import',
key: '3'
});
expect(fixed.composition).toContain({
namespace: 'my-import',
key: '1'
});
});
it('rewrites locations', function () {
expect(swg.location).toBe('my-import:root');
expect(layout.location).toBe('my-import:root');
expect(fixed.location).toBe('my-import:2');
});
it('rewrites matched identifiers in objects', function () {
expect(layout.configuration.layout.panels['my-import:1'])
.toBeDefined();
expect(layout.configuration.layout.panels['my-import:3'])
.toBeDefined();
expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'])
.not.toBeDefined();
expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'])
.not.toBeDefined();
expect(fixed.configuration['fixed-display'].elements[0].id)
.toBe('my-import:1');
});
});
});

View File

@ -0,0 +1 @@
{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"}

View File

@ -88,7 +88,7 @@ define([], function () {
}
getContextMenuActions() {
return ['viewDatumAction'];
return ['viewDatumAction', 'viewHistoricalData'];
}
}

View File

@ -175,14 +175,22 @@ export default {
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) {
showContextMenu: async function (event) {
event.preventDefault();
this.updateViewContext();
this.markRow(event);
const contextualDomainObject = await this.row.getContextualDomainObject?.(this.openmct, this.row.objectKeyString);
let objectPath = this.objectPath;
if (contextualDomainObject) {
objectPath = objectPath.slice();
objectPath.unshift(contextualDomainObject);
}
const actions = this.row.getContextMenuActions().map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
const menuItems = this.openmct.menus.actionsToMenuItems(actions, objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}

View File

@ -21,60 +21,71 @@
*****************************************************************************/
<template>
<div
class="c-conductor"
ref="timeConductorOptionsHolder"
class="c-compact-tc is-expanded"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
style="overflow: visible"
>
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:input-bounds="viewBounds"
@updated="saveClockOffsets"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
<ConductorModeIcon class="c-conductor__mode-icon" />
<!-- TODO - NEED TO ADD MODE, CLOCK AND TIMESYSTEM VIEW ONLY INFORMATION HERE -->
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
:read-only="true"
/>
<conductor-inputs-realtime
v-else
:input-bounds="viewBounds"
:read-only="true"
/>
<conductor-axis
v-if="isFixed"
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
<div
v-else
class="u-flex-spreader"
></div>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<conductor-pop-up
v-if="showConductorPopup"
ref="conductorPopup"
:bottom="false"
:position-x="positionX"
:position-y="positionY"
:is-fixed="isFixed"
@popupLoaded="initializePopup"
@modeUpdated="saveMode"
@clockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div>
</template>
<script>
import _ from 'lodash';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import { FIXED_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
import conductorPopUpManager from "./conductorPopUpManager";
import ConductorPopUp from "./ConductorPopUp.vue";
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -82,24 +93,24 @@ export default {
components: {
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
ConductorPopUp
},
mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'],
data() {
let bounds = this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let timeSystem = this.openmct.time.timeSystem();
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
const isFixed = this.openmct.time.isFixed();
const bounds = this.openmct.time.getBounds();
const offsets = this.openmct.time.getClockOffsets();
const timeSystem = this.openmct.time.getTimeSystem();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
timeSystem,
timeFormatter,
durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
@ -116,37 +127,41 @@ export default {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isFixed,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false,
showConductorPopup: false,
altPressed: false,
isPanning: false,
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
isZooming: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
// this.openmct.time.on('clockChanged', this.setViewFromClock);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
// this.openmct.time.off('clockChanged', this.setViewFromClock);
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
handleNewBounds(bounds, isTick) {
if (this.openmct.time.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
}
},
setBounds(bounds) {
this.bounds = bounds;
@ -195,9 +210,8 @@ export default {
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
setMode(mode) {
this.isFixed = mode === FIXED_MODE_KEY;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
@ -210,11 +224,20 @@ export default {
format: key
}).formatter;
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
},
saveFixedOffsets(bounds) {
saveFixedBounds(bounds) {
this.openmct.time.bounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.setClockOffsets(offsets);
},
saveClock(clockOptions) {
this.openmct.time.setClock(clockOptions.clockKey, clockOptions.offsets);
},
saveMode(mode) {
this.openmct.time.setMode(mode);
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -38,6 +38,7 @@ import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from './utcMultiTimeFormat.js';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -92,13 +93,16 @@ export default {
this.axisElement = vis.append("g")
.attr("class", "axis");
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setViewFromTimeSystem(this.openmct.time.getTimeSystem());
this.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
},
methods: {
setAxisDimensions() {
@ -113,7 +117,7 @@ export default {
return;
}
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (timeSystem.isUTCBased) {
this.xScale.domain(
@ -153,7 +157,7 @@ export default {
this.setScale();
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
@ -224,7 +228,7 @@ export default {
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
@ -291,7 +295,7 @@ export default {
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta;

View File

@ -0,0 +1,142 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
ref="clockButton"
class="c-tc-input-popup__options"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-button--compact js-clock-button"
:class="[
buttonCssClass,
selectedClock.cssClass
]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
</template>
<script>
import modeMixin from './mode-mixin';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
mixins: [modeMixin],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
mounted: function () {
this.loadClocks(this.configuration.menuOptions);
// this.setOffsets();
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
// setOffsets() {
// if (!this.openmct.time.getClockOffsets()) {
// const activeClock = this.getActiveClock();
// const clockConfig = this.getMatchingConfig({
// clock: activeClock.key
// });
// this.openmct.time.setClockOffsets(clockConfig.clockOffsets);
// }
// },
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__clock-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
setClock(clockKey) {
const option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.getTimeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
// this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
const offsets = this.openmct.time.getClockOffsets() ?? configuration.clockOffsets;
option.offsets = offsets;
this.$emit('clockUpdated', option);
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.activeClock = clock;
}
}
};
</script>

View File

@ -26,8 +26,9 @@
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-button--compact c-history-button icon-history"
:class="buttonCssClass"
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
@ -64,6 +65,13 @@ export default {
mode: {
type: String,
required: true
},
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data() {
@ -81,7 +89,7 @@ export default {
*/
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
isFixed: this.openmct.time.getClock() === undefined
};
},
computed: {
@ -95,7 +103,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
if (this.openmct.time.isRealTime()) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@ -106,8 +114,8 @@ export default {
bounds: {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.updateMode();
this.updateMode();
if (this.openmct.time.isFixed()) {
this.addTimespan();
}
},
@ -116,7 +124,9 @@ export default {
offsets: {
handler() {
this.updateMode();
this.addTimespan();
if (this.openmct.time.isRealTime()) {
this.addTimespan();
}
},
deep: true
},
@ -140,7 +150,6 @@ export default {
},
methods: {
updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
@ -151,7 +160,7 @@ export default {
const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
if (this.timeSystem.isUTCBased && !this.openmct.time.isRealTime()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
} else {
name = description;
@ -201,10 +210,11 @@ export default {
},
addTimespan() {
const key = this.timeSystem.key;
const isFixed = this.openmct.time.isFixed();
let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
start: isFixed ? this.bounds.start : this.offsets.start,
end: isFixed ? this.bounds.end : this.offsets.end
};
// no dupes
@ -219,10 +229,10 @@ export default {
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan);
if (this.openmct.time.isFixed()) {
this.openmct.time.getBounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
this.openmct.time.getClockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
@ -259,7 +269,7 @@ export default {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (!this.openmct.time.isFixed()) {
if (time < 0) {
isNegativeOffset = true;
}

View File

@ -1,78 +1,35 @@
<template>
<form
ref="fixedDeltaInput"
class="c-conductor__inputs"
<time-popup-fixed
v-if="readOnly === false"
:input-bounds="bounds"
:input-time-system="timeSystem"
@focus.native="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
>
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">
Start
</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
End
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
</form>
<div class="c-compact-tc__bounds__value">{{ formattedBounds.start }}</div>
<div class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div class="c-compact-tc__bounds__value">{{ formattedBounds.end }}</div>
</div>
</template>
<script>
import DatePicker from "./DatePicker.vue";
import TimePopupFixed from "./timePopupFixed.vue";
import _ from "lodash";
import { TIME_CONTEXT_EVENTS } from "../../api/time/constants";
const DEFAULT_DURATION_FORMATTER = 'duration';
// const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
TimePopupFixed
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
@ -84,18 +41,29 @@ export default {
default() {
return [];
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
const timeSystem = this.openmct.time.getTimeSystem();
// let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.inputBounds || this.openmct.time.getBounds();
console.log('fixed input bounds', this.inputBounds);
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeSystem: timeSystem,
// durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
@ -109,8 +77,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -122,40 +97,30 @@ export default {
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.handleNewBounds(this.timeContext.getBounds());
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
}
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
@ -166,8 +131,8 @@ export default {
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
// this.durationFormatter = this.getFormatter(
// timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
@ -175,116 +140,15 @@ export default {
format: key
}).formatter;
},
setBoundsFromView($event) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.$emit('updated', {
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView());
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
setBoundsFromView(bounds) {
console.log('conductor fixed bounds set bounds from view', bounds);
this.$emit('boundsUpdated', {
start: bounds.start,
end: bounds.end
});
},
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
dismiss() {
this.$emit('dismissInputsFixed');
}
}
};

View File

@ -1,95 +1,44 @@
<template>
<form
ref="deltaInput"
class="c-conductor__inputs"
<time-popup-realtime
v-if="readOnly === false"
:offsets="offsets"
@focus.native="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
>
<div class="c-compact-tc__bounds__value icon-minus">{{ offsets.start }}</div>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta"
>
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
</button>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
Current
</div>
<input
ref="endDate"
v-model="formattedCurrentValue"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
>
</div>
v-if="compact"
class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"
></div>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta"
v-else
class="c-compact-tc__current-update"
>
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}
</button>
LAST UPDATE {{ formattedBounds.end }}
</div>
</form>
<div class="c-compact-tc__bounds__value icon-plus">{{ offsets.end }}</div>
</div>
</template>
<script>
import timePopup from "./timePopup.vue";
import TimePopupRealtime from "./timePopupRealtime.vue";
import _ from "lodash";
import { TIME_CONTEXT_EVENTS } from "../../api/time/constants";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
timePopup
TimePopupRealtime
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
@ -101,15 +50,27 @@ export default {
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue();
const timeSystem = this.openmct.time.getTimeSystem();
const durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const bounds = this.bounds ?? this.openmct.time.getBounds();
const offsets = this.offsets ?? this.openmct.time.getClockOffsets();
const currentValue = this.openmct.time.getClock()?.currentValue();
return {
showTCInputStart: false,
@ -134,8 +95,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -146,45 +114,50 @@ export default {
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTime();
},
methods: {
followTime() {
this.handleNewBounds(this.timeContext.bounds());
this.setViewFromOffsets(this.timeContext.clockOffsets());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.timeContext.on('clockOffsets', this.setViewFromOffsets);
const bounds = this.timeContext ? this.timeContext.getBounds() : this.openmct.time.getBounds();
const offsets = this.timeContext ? this.timeContext.getClockOffsets() : this.openmct.time.getClockOffsets();
this.handleNewBounds(bounds);
this.setViewFromOffsets(offsets);
if (this.timeContext) {
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.followTime();
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
},
clearAllValidation() {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
handleNewBounds(bounds, isTick) {
if (this.timeContext.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
}
},
setViewFromOffsets(offsets) {
if (offsets) {
@ -200,7 +173,7 @@ export default {
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue();
const currentValue = this.openmct.time.getClock()?.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
@ -222,86 +195,25 @@ export default {
format: key
}).formatter;
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
timePopUpdate({ start, end }) {
this.offsets.start = [start.hours, start.minutes, start.seconds].join(':');
this.offsets.end = [end.hours, end.minutes, end.seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
},
setOffsetsFromView($event) {
if (this.$refs.deltaInput.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
setOffsetsFromView() {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', {
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
this.$emit('offsetsUpdated', {
start: startOffset,
end: endOffset
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
dismiss() {
this.$emit('dismissInputsRealtime');
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -22,11 +22,15 @@
<template>
<div
ref="modeButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
class="c-tc-input-popup__options"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-mode-button"
class="c-button--menu c-button--compact js-mode-button"
:class="[
buttonCssClass,
selectedMode.cssClass
]"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
@ -36,32 +40,23 @@
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
import modeMixin from './mode-mixin';
const TEST_IDS = true;
export default {
mixins: [toggleMixin],
mixins: [modeMixin],
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
activeClock = Object.create(activeClock);
}
const mode = this.openmct.time.getMode();
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {}
selectedMode: this.getModeMetadata(mode, TEST_IDS),
modes: []
};
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
this.loadModes();
},
methods: {
showModesMenu() {
@ -74,108 +69,11 @@ export default {
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
loadClocksFromConfiguration() {
let clocks = this.configuration.menuOptions
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined]
.concat(clocks)
.map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
testId: 'conductor-modeOption-fixed',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
testId: 'conductor-modeOption-realtime',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
if (clockKey === 'fixed') {
clockKey = undefined;
}
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.timeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
this.openmct.time.clock(clockKey, offsets);
}
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedMode = this.getModeOptionForClock(clock);
setMode(modeKey) {
this.selectedMode = this.getModeMetadata(modeKey, TEST_IDS);
this.$emit('modeUpdated', modeKey);
}
}
};

View File

@ -21,6 +21,17 @@
*****************************************************************************/
<template>
<div class="c-clock-symbol">
<svg
class="c-clock-symbol__outer"
viewBox="0 0 16 16"
>
<path
d="M6 0L3 0C1.34315 0 0 1.34315 0 3V13C0 14.6569 1.34315 16 3 16H6V13H3V3H6V0Z"
/>
<path
d="M10 13H13V3H10V0H13C14.6569 0 16 1.34315 16 3V13C16 14.6569 14.6569 16 13 16H10V13Z"
/>
</svg>
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>

View File

@ -0,0 +1,261 @@
<template>
<div
class="c-tc-input-popup"
:class="modeClass"
:style="position"
@click.stop
>
<div
class="c-tc-input-popup__options"
>
<IndependentMode
v-if="isIndependent"
class="c-button--compact c-conductor__mode-select"
:mode="timeOptionMode"
:button-css-class="'c-button--compact'"
@independentModeUpdated="saveIndependentMode"
/>
<ConductorMode
v-else
class="c-conductor__mode-select"
:button-css-class="'c-icon-button'"
@modeUpdated="saveMode"
/>
<IndependentClock
v-if="isIndependent"
class="c-conductor__mode-select"
:clock="timeOptionClock"
:button-css-class="'c-icon-button'"
@independentClockUpdated="saveIndependentClock"
/>
<ConductorClock
v-else
class="c-conductor__mode-select"
:button-css-class="'c-icon-button'"
@clockUpdated="saveClock"
/>
<!-- TODO: Time system and history must work even with ITC later -->
<ConductorTimeSystem
v-if="!isIndependent"
class="c-conductor__time-system-select"
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
:button-css-class="'c-icon-button'"
:offsets="timeOffsets"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="bounds"
:object-path="objectPath"
@boundsUpdated="saveFixedBounds"
@dismissInputsFixed="dismiss"
/>
<conductor-inputs-realtime
v-else
:input-bounds="bounds"
:object-path="objectPath"
@offsetsUpdated="saveClockOffsets"
@dismissInputsRealtime="dismiss"
/>
</div>
</template>
<script>
import ConductorMode from './ConductorMode.vue';
import ConductorClock from './ConductorClock.vue';
import IndependentMode from './independent/IndependentMode.vue';
import IndependentClock from './independent/IndependentClock.vue';
import ConductorTimeSystem from "./ConductorTimeSystem.vue";
import ConductorHistory from "./ConductorHistory.vue";
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
import { TIME_CONTEXT_EVENTS, REALTIME_MODE_KEY, FIXED_MODE_KEY } from '../../api/time/constants';
export default {
components: {
ConductorMode,
ConductorClock,
IndependentMode,
IndependentClock,
ConductorTimeSystem,
ConductorHistory,
ConductorInputsFixed,
ConductorInputsRealtime
},
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
positionX: {
type: Number,
required: true
},
positionY: {
type: Number,
required: true
},
isFixed: {
type: Boolean,
required: true
},
isIndependent: {
type: Boolean,
default() {
return false;
}
},
timeOptions: {
type: Object,
default() {
return undefined;
}
},
bottom: {
type: Boolean,
default() {
return false;
}
},
objectPath: {
type: Array,
default() {
return [];
}
}
},
data() {
const bounds = this.openmct.time.getBounds();
const timeSystem = this.openmct.time.getTimeSystem();
// const isFixed = this.openmct.time.isFixed();
return {
timeSystem,
bounds: {
start: bounds.start,
end: bounds.end
}
};
},
computed: {
position() {
const position = {
left: `${this.positionX}px`
};
if (this.isIndependent) {
position.top = `${this.positionY}px`;
}
return position;
},
timeOffsets() {
return this.isFixed ? this.openmct.time.getBounds() : this.openmct.time.getClockOffsets();
},
timeMode() {
return this.isFixed ? FIXED_MODE_KEY : REALTIME_MODE_KEY;
},
modeClass() {
const value = this.bottom ? 'c-tc-input-popup--bottom' : '';
return this.isFixed ? `${value} c-tc-input-popup--fixed-mode` : `${value} c-tc-input-popup--realtime-mode`;
},
timeOptionMode() {
return this.timeOptions?.mode;
},
timeOptionClock() {
return this.timeOptions?.clock;
}
},
watch: {
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}
},
mounted() {
this.$emit('popupLoaded');
this.setTimeContext();
},
beforeDestroy() {
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
this.setViewFromClock(this.timeContext.getClock());
this.setBounds(this.timeContext.getBounds());
},
stopFollowingTimeContext() {
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
},
setViewFromClock() {
this.bounds = this.isFixed ? this.timeContext.getBounds() : this.openmct.time.getClockOffsets();
console.log('set view from clock popup', this.bounds);
},
setBounds(bounds, isTick) {
if (this.isFixed || !isTick) {
console.log('set bounds popup', bounds);
this.bounds = bounds;
}
},
setMode(mode) {
// this.isFixed = mode === FIXED_MODE_KEY;
},
saveFixedBounds(bounds) {
this.$emit('fixedBoundsUpdated', bounds);
},
saveClockOffsets(offsets) {
this.$emit('clockOffsetsUpdated', offsets);
},
saveClock(clockOptions) {
this.$emit('clockUpdated', clockOptions);
},
saveMode(mode) {
this.$emit('modeUpdated', mode);
},
saveIndependentMode(mode) {
this.$emit('independentModeUpdated', mode);
},
saveIndependentClock(clockKey) {
this.$emit('independentClockUpdated', clockKey);
},
dismiss() {
this.$emit('dismiss');
}
}
};
</script>

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