Compare commits

..

84 Commits

Author SHA1 Message Date
Michael Rogers
3ba514e551 Removed set in favor of using isNavigatedObject api 2022-12-13 21:02:13 -06:00
Michael Rogers
739d55b357 Merge branch 'master' into mct5867 2022-12-13 20:44:35 -06:00
Jesse Mazzella
d54335d21c Bump version to 2.1.5-SNAPSHOT (#6052) 2022-12-12 15:57:03 -08:00
Khalid Adil
e0ed0bb6e2 [Plots] Ignore Infinity when autoscaling y-axis (#5907)
* Change approach to filter positive and negative infinity values when updating stats

* Change filter when there are no plot stats and a positive/negative infinity value occurs

* Add check for negative infinity

* Name the unplottable values array and move it to the constructor

* Add option to render infinity values

* Add e2e test to render plot with infinity values

* Add accessibility labels to help locate items in tests
Refactor tests

* refactor(e2e): stabilize plotRendering test

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-12 11:51:57 -08:00
dependabot[bot]
ed3fd8f965 Bump babel-loader from 9.0.0 to 9.1.0 (#5947)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 9.0.0 to 9.1.0.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v9.0.0...v9.1.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 22:45:25 +00:00
dependabot[bot]
e6d59c61d1 Bump typescript from 4.9.3 to 4.9.4 (#6046)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.3 to 4.9.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.3...v4.9.4)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 14:33:49 -08:00
Kierstyn Just
b74b27c464 docs: fixed punctuation & grammar in summary section (#6037) 2022-12-06 23:59:54 +00:00
dependabot[bot]
d35e161701 Bump @types/jasmine from 4.3.0 to 4.3.1 (#6040)
Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 18:08:24 +00:00
dependabot[bot]
653cb62f9c Bump mini-css-extract-plugin from 2.6.1 to 2.7.2 (#6043)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.6.1 to 2.7.2.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.6.1...v2.7.2)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 11:00:34 +01:00
dependabot[bot]
19b3232fa0 Bump eslint from 8.28.0 to 8.29.0 (#6041)
Bumps [eslint](https://github.com/eslint/eslint) from 8.28.0 to 8.29.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.28.0...v8.29.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-05 16:56:58 -08:00
Khalid Adil
19892aab53 [MMGIS] Modify Independent time contexts, open in new tab action, and expose overlay plots to support MMGIS pivoting (#6025)
* Expose overlay plot so that it can be imported by an external plugin

* If the current object has an independentContext, ignore any upstream independentContext

* Accept any custom url param in open in new tab action
2022-12-02 17:03:43 -06:00
dependabot[bot]
a168ce25cf Bump eslint-plugin-vue from 9.7.0 to 9.8.0 (#6012)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.7.0 to 9.8.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.7.0...v9.8.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-02 21:44:49 +00:00
Michael Rogers
189c58f952 Remove Go To Original when in edit mode (#5966)
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2022-12-02 21:16:36 +00:00
dependabot[bot]
0dfc028e1b Bump @types/lodash from 4.14.190 to 4.14.191 (#6027)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.190 to 4.14.191.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-01 13:32:36 -08:00
dependabot[bot]
77e93f1aee Bump eslint from 8.27.0 to 8.28.0 (#6002)
Bumps [eslint](https://github.com/eslint/eslint) from 8.27.0 to 8.28.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.27.0...v8.28.0)

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

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

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>
2022-11-30 22:29:47 +00:00
dependabot[bot]
394fbbe61b Bump @types/lodash from 4.14.189 to 4.14.190 (#6010)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.189 to 4.14.190.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-30 18:18:36 +00:00
Jesse Mazzella
40afb04f0c Merge release/2.1.3 into master (#6015)
* Bump version to `2.1.3` (#5973)
* Preserve Gauge configuration changes on create/edit (#5986)
* fix(#5985): deep merge on create/edit properties
- Perform a deep merge of old and new properties on Create/Edit properties actions
* refactor(e2e): improve selector in appActions
* test(e2e): add tests for gauges
- test creating a non-default gauge (checks only for console errors)
- test updating a gauge (checks only for console errors)
* fix(e2e): use pluginFixtures for gauge tests
* fix(e2e): prevent fail if testNotes is undefined
* Make the tree key unique (#5989)

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-11-29 17:51:43 -08:00
Andrew Henry
be73b0158a Remove link to retired live demo (#6008)
* Remove link to retired live demo
Closes https://github.com/nasa/openmct/issues/6006
Remove link to retired live demo
* Added screenshot
2022-11-28 17:06:55 +00:00
David Tsay
625205f24b do not use dev source maps in prod (#6007) 2022-11-28 12:00:18 -05:00
dependabot[bot]
a706a8b73e Bump @percy/cli from 1.13.0 to 1.16.0 (#6011) 2022-11-24 13:02:02 -08:00
子瞻 Luci
1ddf5e5137 feat(imagery): show viewable area when zoomed (#5877)
* feat: viewable area

* chore: add test

* fix: get image ref when real-time

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-11-21 13:06:12 -08:00
dependabot[bot]
a79646a915 Bump typescript from 4.8.4 to 4.9.3 (#5988)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.8.4 to 4.9.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-19 00:04:16 +00:00
dependabot[bot]
d5266e7ac7 Bump webpack-cli from 4.10.0 to 5.0.0 (#5996)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.10.0 to 5.0.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.10.0...webpack-cli@5.0.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 19:25:36 +00:00
Michael Rogers
581d4fb2d1 Logical or to nullish coalescing 2022-11-17 11:54:02 -06:00
dependabot[bot]
05de7ee2e0 Bump @braintree/sanitize-url from 6.0.0 to 6.0.2 (#5970)
Bumps [@braintree/sanitize-url](https://github.com/braintree/sanitize-url) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/braintree/sanitize-url/releases)
- [Changelog](https://github.com/braintree/sanitize-url/blob/main/CHANGELOG.md)
- [Commits](https://github.com/braintree/sanitize-url/compare/v6.0.0...v6.0.2)

---
updated-dependencies:
- dependency-name: "@braintree/sanitize-url"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-17 09:48:23 -08:00
Michael Rogers
41a6b7582e await scrollToFocused 2022-11-17 11:48:11 -06:00
Scott Bell
d92e00f2dc Merge branch 'master' into mct5867 2022-11-16 14:49:50 +01:00
dependabot[bot]
dad88112c4 Bump @types/lodash from 4.14.188 to 4.14.189 (#5987) 2022-11-15 13:42:48 -08:00
Scott Bell
71ef809207 Merge branch 'master' into mct5867 2022-11-15 12:15:01 +01:00
Jesse Mazzella
202d6d8c5d Bump version to 2.1.4-SNAPSHOT (#5974) 2022-11-14 10:48:36 -08:00
David Tsay
e70bcc414c revert persisted time to last actual persisted time on save error (#5971) 2022-11-10 15:25:35 -08:00
Jesse Mazzella
7bb4a136d7 Use Composition API to add/remove from composition (#5941)
* Use composition API in RemoveAction

* refactor: ScatterPlotView to use composition API

* fix: initialize transaction to null and reset

* fix: remove seriesKey and correct found condition

* refactor: Gauge to use composition API

* refactor: DisplayLayout to use composition API

* test: RemoveAction starts and ends transactions

* test: add ScatterPlot add/remove telemetry test

* test: fix e2e test and add annotation

* test: remove unnecessary awaits

* test: make some displayLayout tests stable

* test{displayLayout}: navigate to objects via url

* test(gauge): add test for add/remove telemetry

* fix(#3117): init layoutItems within transaction

* refactor: add clearSelection() method

* test: remove unstable tag

* fix(#3117): init frames and use transactions

- fixes #3117 for flexible layouts by syncing frames and composition

- also uses transactions now to avoid race condition

* test(flexibleLayout): removing items via context menu

- add test for removing items via context menu while focusing the layout

- add test for removing items via context menu while not focusing the layout

* fix(e2e): use pluginFixtures

* refactor(e2e): improve selectors

* refactor: use async/await for saving transactions

* docs(e2e): fix comments

* test: use soft expects

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-11-10 20:06:13 +00:00
tobiasbrown
8af3b4309f Add 'View type' label to Display Layout toolbar (#5909)
Addresses #5480

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-11-10 11:38:05 -08:00
dependabot[bot]
bed3d83fd7 Bump moment-timezone from 0.5.37 to 0.5.38 (#5881)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.37 to 0.5.38.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.37...0.5.38)

---
updated-dependencies:
- dependency-name: moment-timezone
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-10 17:06:52 +00:00
John Hill
efda42cf6d Make package public - master (#5967)
* Update package.json

* Update npm-prerelease.yml
2022-11-10 08:45:55 -08:00
Jesse Mazzella
e8ee5b3fc9 fix test flake: calculate persisted time last (#5923)
* fix: calculate persisted time last
2022-11-10 08:40:09 -08:00
dependabot[bot]
393cb9767f Bump sass from 1.55.0 to 1.56.1 (#5963) 2022-11-10 16:26:19 +00:00
Jesse Mazzella
8b5daad65c feat: sort interceptors by priority, ensure myItemsInterceptor runs first (#5965)
* feat: sort interceptors by priority

* fix(#5914): high priority for MyItemsInterceptor

* fix: create myItems if object is falsy

* test(e2e): update snapshots

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-11-10 08:00:29 -08:00
Michael Rogers
7b01b955ac Prevent default for arrow keys to avoid scrolling layoyut 2022-11-09 14:31:57 -08:00
Michael Rogers
eb36c95035 Remove done invocation after converting to async fn 2022-11-09 13:47:06 -08:00
Michael Rogers
72812673d0 Adjust test to capture new scroll handler 2022-11-09 13:41:59 -08:00
Michael Rogers
490c3ab8dd Simplified scroll reset event and logic 2022-11-09 12:35:26 -08:00
Michael Rogers
5aed0f3637 Implement a separate scroll to action when in layouts 2022-11-08 17:06:32 -08:00
Scott Bell
fabfecdb3e Check for null plot wrapper on plot resize (#5960)
* check for null plot wrapper first

* make code clearer with short circuit up front
2022-11-08 16:11:46 -08:00
Michael Rogers
302351685d Setup a scroll handler to avoid using scrollIntoView when in a layout 2022-11-08 15:58:54 -08:00
dependabot[bot]
a2d8b13204 Bump eslint from 8.26.0 to 8.27.0 (#5955)
Bumps [eslint](https://github.com/eslint/eslint) from 8.26.0 to 8.27.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.26.0...v8.27.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-08 18:14:08 +00:00
Jesse Mazzella
4b14d2d6d2 [e2e][couchdb] Add test coverage for mct5616 (#5913)
* [e2e][couchdb] Add test for mct5616

- Add test to verify that modifying object properties generates a single persistence operation with CouchDB

* [e2e] Poll for 1 sec to allow time for request

* [e2e] ignore expected console error

* add comments

* add `aria-label`s to form select fields

* add `aria-label` for clock format select

* [e2e] Improve selector

* make property name less ambiguous

* refactor(e2e): use default object name
2022-11-07 16:04:02 -08:00
Jesse Mazzella
d545124942 test(e2e): unique names for created objects by default (#5945)
* feat(e2e): default unique names for new objects

* refactor(e2e): reference generated object names

- Fixes the tests that were locating "Unnamed <object_type>" to use the generated unique names

* feat(e2e): add testInfo into domainObject notes

- adds info about the currently running test and its project to notes

* fix(e2e): fix selector for notes section

* feat: ARIA: menu role for menus and SuperMenus

- Implements the [ARIA: menu role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/roles/menu_role) for Menus and SuperMenus,

* refactor(e2e): use role selectors for menu items

* refactor(e2e): better selectors for "OK" button

* refactor(e2e): better selectors for menu items

* refactor(e2e): improve selector

* refactor(e2e): update test to use appActions

* refactor(e2e): update test to use object name

* refactor(e2e): improve selectors for menu items

* test(e2e): fix search test

* refactor(e2e): update more plain 'text=' selectors

* fix: resolve codeQL error

- remove superfluous argument

* refactor(e2e): move testNotes to `pluginFixtures` and update imports

* refactor(e2e): remove unused fixture from test

* refactor: add dynamic id to form textareas

* refactor(e2e): improve notes textarea selector

* refactor(e2e): remove unused fixture
2022-11-07 23:50:33 +00:00
John Hill
6abdbfdff0 skip one mo (#5958) 2022-11-07 23:39:01 +00:00
dependabot[bot]
500e655476 Bump jasmine-core from 4.4.0 to 4.5.0 (#5934) 2022-11-04 17:14:07 +00:00
dependabot[bot]
5e1f026db2 Bump @types/lodash from 4.14.186 to 4.14.188 (#5950) 2022-11-04 10:07:49 -07:00
dependabot[bot]
d9efae98c8 Bump @percy/cli from 1.11.0 to 1.13.0 (#5942) 2022-11-04 16:50:42 +00:00
Jesse Mazzella
091f6406a8 Merge release/2.1.2 into master (#5946)
* Bump version to `2.1.2`

* Ensure properties stay in sync and are committed only once (#5717)

* Ensure form properties stay in sync
* Separate out overlay based forms and custom forms
* Use a transaction to save properties
* Fix GaugeController to not depend on event emitted from FormsAPI
* refactor showForms to call showCustomForm

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Fix persistence timestamp (#5916)

* Calculate persisted timestamp last
* Added regression tests
* Correct transaction handling code
* Code cleanup

* Fix typo for publish (#5936)

* Add e2e tests to npm package (#5930)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-11-03 13:49:03 -07:00
dependabot[bot]
42a0e503cc Bump eslint-plugin-vue from 9.6.0 to 9.7.0 (#5932)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.6.0 to 9.7.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.6.0...v9.7.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-01 16:41:03 +00:00
Shefali Joshi
4697352f60 Fix now marker for time system axis in timestrips and plans (#5911)
* Ensure the now marker spans the height of the plan.
* Set height for timestrip for the now marker
* Fix linting issues
* Fix failing test
2022-10-31 23:56:52 +00:00
John Hill
015c764ab3 Update dependabot.yml (#5935) 2022-10-31 21:58:12 +00:00
dependabot[bot]
8fe465d9fc Bump eslint from 8.25.0 to 8.26.0 (#5908)
Bumps [eslint](https://github.com/eslint/eslint) from 8.25.0 to 8.26.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.25.0...v8.26.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-28 21:57:31 +00:00
dependabot[bot]
9c1368885a Bump babel-loader from 8.2.5 to 9.0.0 (#5920) 2022-10-27 14:51:01 -07:00
Jesse Mazzella
391c0b2e7c Bump version to 2.1.3-SNAPSHOT (#5904) 2022-10-27 17:29:41 +00:00
dependabot[bot]
2ae061dbcd Bump sinon from 14.0.0 to 14.0.1 (#5831)
Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.0 to 14.0.1.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v14.0.0...v14.0.1)
Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-25 11:42:08 -07:00
Jesse Mazzella
41fc502564 Generate type declarations for CompositionAPI and publish with OpenMCT (#5838)
* add typescript

* update tsconfig

* convert to es6 class

* Convert more stuff to es6 class

* skip checking libs, test files

* more es6 classes!

* Fix some jsdocs

* Rename file

* Improve jsdoc types

* Rename references as well

* more types

* types for CompositionAPI

* Types for CompositionCollection

* Types for CompositionProvider

* type

* types for api

* nvm

* cleanup MCT

* Fix API type definition

* Generate types before publish

* fix openmct 👀

* rename PublicAPI -> OpenMCT and document methods

* try and fix visual test ?

* Make private methods private

* more private methods!!

* import all es6 api's so we get more types for free

* convert Selection to es6 class

* remove redundant docs

* fix Branding types

* fix openmct.start() types

* Remove useless `@memberof`

* Add parameter name

* [docs] Add a section on Types

* markdownlint

* word

* Add section on limitations / contibuting types

* Let these methods be private

* make private members private, fix a type

* fix another type

* Make method private

* Update docs for `skipMutate` and related methods

* Rename file and fix references

* `DefaultCompositionProvider` extends `CompositionProvider`

* Make private members private

* Type for `AbortSignal`

* `domainObject` must be accessible for perf tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-21 17:29:52 -07:00
Jamie V
b4554d2fc1 User attribution (#5827)
* initial changes adding modified and created by fields to domain objects and updating properties inspector
* adding created date to object creation
* added a test for created timestamp
* updating remove action to hold the transaction and disregard edit state when handling transactions, also updated object api to return transaction when starting and ignore edit state when determining if transaction is active
* updating docs for object api "startTransaction"
* updating incorrect use of edit and transaction in our appActions for testing

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
2022-10-21 17:05:59 -07:00
David Tsay
feba5f6d3b use full identifier instead of key (#5891) 2022-10-21 16:45:52 -07:00
tobiasbrown
4357d35f4a Centre and wrap Condition Widget label (#5886)
* [ConditionWidget] Center label text

Addresses #5799

* [ConditionWidget] Wrap label text

Addresses #5799

* [ConditionWidget] Add padding to label

Addresses #5799

* [ConditionWidget] Use interiorMargin value for padding

Addresses #5799
2022-10-21 16:35:59 -07:00
Jesse Mazzella
5041f80e5b Update version to 2.1.2-SNAPSHOT (#5897) 2022-10-21 12:16:46 -07:00
Scott Bell
9e23f79bc8 Add time context to telemetry requests (#5887)
* add time context to telemetry requests

* change to empty array

* refactor telemetry api to use time context

* removed unused function

* add tests

* add test, rename function

* make function public
2022-10-21 20:25:24 +02:00
Scott Bell
bd1e869f6a fix timing issue (#5896) 2022-10-21 20:19:41 +02:00
dependabot[bot]
e4a36532e7 Bump @types/lodash from 4.14.178 to 4.14.186 (#5892) 2022-10-19 20:40:30 +00:00
dependabot[bot]
2bc2316613 Bump @types/jasmine from 4.0.1 to 4.3.0 (#5893) 2022-10-19 13:34:44 -07:00
John Hill
2fa36b2176 Delete lighthouse.yml (#5885) 2022-10-18 16:10:09 -07:00
John Hill
efa38d779e Remove two types from package and pin others (#5883)
* Update package.json

* Update package.json

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-10-18 21:17:30 +00:00
dependabot[bot]
951cc6ec0d Bump eslint-plugin-vue from 9.3.0 to 9.6.0 (#5837)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.3.0 to 9.6.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.3.0...v9.6.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 16:31:01 +00:00
dependabot[bot]
ef4b8a9934 Bump @percy/cli from 1.10.3 to 1.11.0 (#5846) 2022-10-17 17:52:11 -07:00
Shefali Joshi
c14b48917e Plan documentation (#5871)
* Plan documentation
2022-10-13 20:04:51 -07:00
dependabot[bot]
26165d0a99 Bump eslint from 8.24.0 to 8.25.0 (#5861)
Bumps [eslint](https://github.com/eslint/eslint) from 8.24.0 to 8.25.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.24.0...v8.25.0)

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

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

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>
2022-10-13 16:53:05 +00:00
John Hill
f7cf3f72c2 Add playwright-core to dependabot ignore list (#5863) 2022-10-10 15:35:40 -07:00
Shefali Joshi
cb8e09c9f9 Master 2.1.1 (#5858)
* Update version

* Don't delete annotations if there aren't any (#5829)

* don't delete annotations if there aren't any

* add test and align playwright-test

* align core with test

* added annotation describing test

* Add `aria-label` for time conductor history button (#5830)

* [Overlay Plot] Inspector series and legend sync fix (#5835)

* fixed overlay plots to react to series removals correctly, added alias visual to elements pool aliased items

* Keep transaction open on failed editor save (#5840)

* do not end a transaction on a failed editor save
* add unit tests for successful editor save and unsuccessful editor save

* If no matching tags, do not attempt tag search (#5839)

* do not attempt search if no matching tags

* fix timing on test

* commit again in hopes that github will run checks

* add back null tag check

* add some better documentation to tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Update version for  master

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-08 09:04:38 -07:00
Michael Rogers
026eb86f5f 4417 fix codeql issues (#5793)
* Add release to codeql and queries to match lgtm

* Add lgtm config file

* Custom codeQL config to ignore app.js

* Custom config for lgtm

* Remove query filter for lgtm

* Updated the security test docs

* Remove lgtm.yml and delete app.js references

* Update codeql-config.yml

Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-07 16:30:09 +00:00
Jesse Mazzella
866859a937 [CouchDB] Re-establish feed connection if EventSource is closed due to error (#5845)
* Re-establish feed connection if EventSource is closed due to error

* Use keepAlive timer

* Get rid of magic numbers and add comment
2022-10-06 21:41:38 +02:00
Jesse Mazzella
afc54f41f6 Publish example layouts json (#5842) 2022-10-05 14:17:36 -07:00
Scott Bell
72c980f991 Have webpack-dev-server write webpack assets to disk (#5836)
* create the files not webpacked and watch css

* removing trailing slash
2022-10-04 10:53:44 -07:00
dependabot[bot]
9bf39a9cd4 Bump eslint from 8.23.1 to 8.24.0 (#5807)
Bumps [eslint](https://github.com/eslint/eslint) from 8.23.1 to 8.24.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.23.1...v8.24.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-03 11:02:01 -07:00
Jamie V
33fd95cb2b Revert "[User Attribution] "createdBy" and "modifiedBy" fields for domainObjects (#5741)" (#5826)
This reverts commit 8c92178895.
2022-09-30 16:53:49 -07:00
Jamie V
8c92178895 [User Attribution] "createdBy" and "modifiedBy" fields for domainObjects (#5741)
* Implementation of user attribution of object changes
* Adds created date to object creation
* Updating remove action to wait for save before navigationg

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-30 13:47:10 -07:00
124 changed files with 5658 additions and 2165 deletions

1
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1 @@
name: 'Custom CodeQL config'

View File

@@ -13,14 +13,18 @@ updates:
- "pr:daveit"
- "pr:platform"
ignore:
#We have to source the container which is not detected by Dependabot
#We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@playwright/test"
#Lots of noise in these type patch releases.
- dependency-name: "playwright-core"
#Lots of noise in these type patch releases.
- dependency-name: "@babel/eslint-parser"
update-types: ["version-update:semver-patch"]
update-types: ["version-update:semver-patch"]
- dependency-name: "eslint-plugin-vue"
update-types: ["version-update:semver-patch"]
- dependency-name: "babel-loader"
update-types: ["version-update:semver-patch"]
- dependency-name: "sinon"
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:

View File

@@ -1,11 +1,10 @@
name: "CodeQL"
name: 'CodeQL'
on:
push:
branches: [ master ]
branches: [master, 'release/*']
pull_request:
branches: [ master ]
branches: [master, 'release/*']
paths-ignore:
- '**/*Spec.js'
- '**/*.md'
@@ -27,17 +26,19 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
languages: javascript
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -1,98 +0,0 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
pull_request:
types:
- labeled
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Master for Baseline
uses: actions/checkout@v3
with:
ref: master #explicitly checkout master for baseline
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline and ignore exit codes
run: lhci autorun || true
- name: Perform clean checkout of PR
uses: actions/checkout@v3
with:
clean: true
- name: Install Node version which is compatible with PR
uses: actions/setup-node@v3
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci with PR
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-nightly:
if: ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-dispatch:
if: ${{ github.event.workflow_dispatch }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun

View File

@@ -16,7 +16,11 @@ jobs:
with:
node-version: 16
- run: npm install
- run: npm test
- run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
npm publish --access=public --tag unstable openmct
# - run: npm test
publish-npm-prerelease:
needs: build
@@ -28,6 +32,6 @@ jobs:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm publish --access public --tag unstable
- run: npm publish --access=public --tag unstable
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -21,4 +21,10 @@
!copyright-notice.html
!index.html
!openmct.js
!SECURITY.md
!SECURITY.md
# Add e2e tests to npm package
!/e2e/**/*
# ... except our test-data folder files.
/e2e/test-data/*.json

594
API.md

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ accept changes from external contributors.
The short version:
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions)
1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
2. Make sure your contribution meets code, test, and commit message
standards as described below.
3. Submit a pull request from a topic branch back to `master`. Include a check

View File

@@ -6,10 +6,8 @@ Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting S
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action
![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
## Building and Running Open MCT Locally
@@ -100,7 +98,7 @@ To run the performance tests:
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
### Security Tests
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
### Test Reporting and Code Coverage

View File

@@ -46,6 +46,7 @@
*/
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
@@ -56,6 +57,10 @@ const Buffer = require('buffer').Buffer;
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
@@ -67,13 +72,18 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li:text("${type}")`);
await page.click(`li[role='menuitem']:text("${type}")`);
// Modify the name input field of the domain object to accept 'name'
if (name) {
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(page.testNotes);
}
// Click OK button and wait for Navigate event
@@ -96,8 +106,8 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
}
return {
name: name || `Unnamed ${type}`,
uuid: uuid,
name,
uuid,
url: objectUrl
};
}
@@ -225,15 +235,14 @@ async function getHashUrlToDomainObject(page, uuid) {
}
/**
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
*/
async function _isInEditMode(page, identifier) {
// eslint-disable-next-line no-return-await
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
return await page.evaluate(() => window.openmct.editor.isEditing());
}
/**

View File

@@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../appActions');
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
@@ -38,24 +40,17 @@ async function enterTextEntry(page, text) {
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
async function dragAndDropEmbed(page, notebookObject) {
// Create example telemetry object
const swg = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator"
});
// Navigate to notebook
await page.goto(notebookObject.url);
// Expand the tree to reveal the notebook
await page.click('button[title="Show selected item in tree"]');
// Drag and drop the SWG into the notebook
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
}
// eslint-disable-next-line no-undef

View File

@@ -126,13 +126,21 @@ exports.test = test.extend({
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow
page: async ({ page, theme }, use) => {
page: async ({ page, theme }, use, testInfo) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') {
//inject snow theme
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
}
// Attach info about the currently running test and its project.
// This will be used by appActions to fill in the created
// domain object's notes.
page.testNotes = [
`${testInfo.titlePath.join('\n')}`,
`${testInfo.project.name}`
].join('\n');
await use(page);
},
myItemsFolderName: [myItemsFolderName, { option: true }],
@@ -140,22 +148,5 @@ exports.test = test.extend({
openmctConfig: async ({ myItemsFolderName }, use) => {
await use({ myItemsFolderName });
}
// objectCreateOptions: [objectCreateOptions, {option: true}],
// eslint-disable-next-line no-shadow
// domainObject: [async ({ page, objectCreateOptions }, use) => {
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
// // eslint-disable-next-line playwright/no-conditional-in-test
// if (objectCreateOptions === null) {
// await use(page);
// return;
// }
// //Go to baseURL
// await page.goto('./', { waitUntil: 'networkidle' });
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
// await use({ uuid });
// }, { auto: true }]
});
exports.expect = expect;

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../baseFixtures.js');
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('AppActions', () => {
@@ -50,11 +50,11 @@ test.describe('AppActions', () => {
});
await page.goto(timer1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
await page.goto(timer2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
await page.goto(timer3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
});
await test.step('Create multiple nested objects in a row', async () => {
@@ -74,11 +74,11 @@ test.describe('AppActions', () => {
parent: folder2.uuid
});
await page.goto(folder1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
await page.goto(folder2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
await page.goto(folder3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);

View File

@@ -45,7 +45,7 @@
*/
// Structure: Some standard Imports. Please update the required pathing.
const { test, expect } = require('../../baseFixtures');
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
/**
@@ -144,5 +144,5 @@ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
// Click Ok button to Save
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
}

View File

@@ -43,14 +43,14 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -58,7 +58,7 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@@ -25,7 +25,7 @@
*
*/
const { test, expect } = require('../../baseFixtures');
const { test, expect } = require('../../pluginFixtures');
test.describe("CouchDB Status Indicator @couchdb", () => {
test.use({ failOnConsoleError: false });

View File

@@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
const { test, expect } = require('../../../baseFixtures');
const { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../appActions');
test.describe('Example Event Generator CRUD Operations', () => {

View File

@@ -96,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
//Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
page.click('button:has-text("OK")')
]);
// Verify that the Sine Wave Generator is displayed and correct

View File

@@ -25,6 +25,7 @@ This test suite is dedicated to tests which verify form functionality in isolati
*/
const { test, expect } = require('../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const path = require('path');
const TEST_FOLDER = 'test folder';
@@ -43,7 +44,7 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation
await expect(page.locator('text=OK')).toBeDisabled();
await expect(page.locator('button:has-text("OK")')).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
//Correct Form Validation for missing title and trigger validation with 'Tab'
@@ -52,13 +53,13 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation is corrected
await expect(page.locator('text=OK')).toBeEnabled();
await expect(page.locator('button:has-text("OK")')).toBeEnabled();
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
//Finish Creating Domain Object
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
page.click('button:has-text("OK")')
]);
//Verify that the Domain Object has been created with the corrected title property
@@ -91,6 +92,44 @@ test.describe('Persistence operations @addInit', () => {
});
});
test.describe('Persistence operations @couchdb', () => {
test.use({ failOnConsoleError: false });
test('Editing object properties should generate a single persistence operation', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5616'
});
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Count all persistence operations (PUT requests) for this specific object
let putRequestCount = 0;
page.on('request', req => {
if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {
putRequestCount += 1;
}
});
// Open the edit form for the clock object
await page.click('button[title="More options"]');
await page.click('li[title="Edit properties of this object."]');
// Modify the display format from default 12hr -> 24hr and click 'Save'
await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' });
await page.click('button[aria-label="Save"]');
await expect.poll(() => putRequestCount, {
message: 'Verify a single PUT request was made to persist the object',
timeout: 1000
}).toEqual(1);
});
});
test.describe('Form Correctness by Object Type', () => {
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});

View File

@@ -81,7 +81,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-move').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
@@ -95,11 +95,11 @@ test.describe('Move & link item tests', () => {
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
@@ -108,7 +108,7 @@ test.describe('Move & link item tests', () => {
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('li[role="menuitem"]:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
@@ -120,7 +120,7 @@ test.describe('Move & link item tests', () => {
// Continue test regardless of assertion and create it in My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
// Open My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
@@ -196,7 +196,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-link').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();

View File

@@ -40,11 +40,11 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")');
await page.locator('li:has-text("Condition Set")').click();
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
page.click('button:has-text("OK")')
]);
//Save localStorage for future test execution
@@ -163,9 +163,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Click hamburger button
await page.locator('[title="More options"]').click();
// Click text=Remove
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
// Click 'Remove' and press OK
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
//Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();

View File

@@ -23,7 +23,7 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Testing Display Layout @unstable', () => {
test.describe('Display Layout', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
@@ -55,12 +55,12 @@ test.describe('Testing Display Layout @unstable', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const formattedTelemetryValue = await getTelemValuePromise;
const formattedTelemetryValue = getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create a Display Layout
@@ -86,12 +86,12 @@ test.describe('Testing Display Layout @unstable', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const formattedTelemetryValue = await getTelemValuePromise;
const formattedTelemetryValue = getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
// Create a Display Layout
@@ -116,16 +116,20 @@ test.describe('Testing Display Layout @unstable', () => {
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// delete
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
@@ -144,18 +148,18 @@ test.describe('Testing Display Layout @unstable', () => {
// Expand the Display Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Click the original Sine Wave Generator to navigate away from the Display Layout
await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
// Go to the original Sine Wave Generator to navigate away from the Display Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
await page.goto(displayLayout.url);
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
});

View File

@@ -23,12 +23,13 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Testing Flexible Layout @unstable', () => {
test.describe('Flexible Layout', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
@@ -54,13 +55,81 @@ test.describe('Testing Flexible Layout @unstable', () => {
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Flexible Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(flexibleLayout.url);
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
});

View File

@@ -0,0 +1,124 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Gauge component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
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' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
// Create the gauge with defaults
const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the gauge
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Navigate to the gauge and verify that
// the SWG appears in the elements pool
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the gauge and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
test('Can create a non-default Gauge', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5356'
});
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Gauge")`);
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
test('Can edit a single Gauge-specific property', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5985'
});
// Create the gauge with defaults
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
await page.click('button[title="More options"]');
await page.click('li[role="menuitem"]:has-text("Edit Properties")');
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
});

View File

@@ -40,10 +40,10 @@ test.describe('Example Imagery Object', () => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object
await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator(backgroundImageSelector).hover({trial: true});
});
@@ -188,7 +188,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -197,7 +197,7 @@ test.describe('Example Imagery in Display Layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
page.click('button:has-text("OK")'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -275,7 +275,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -284,7 +284,7 @@ test.describe('Example Imagery in Flexible layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
page.click('button:has-text("OK")'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -317,7 +317,7 @@ test.describe('Example Imagery in Tabs View', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -326,7 +326,7 @@ test.describe('Example Imagery in Tabs View', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
page.click('button:has-text("OK")'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -26,7 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');

View File

@@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
*/
const { test, expect } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook Tests with CouchDB @couchdb', () => {

View File

@@ -36,27 +36,27 @@ test.describe('Restricted Notebook', () => {
});
test('Can be renamed @addInit', async ({ page }) => {
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
});
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
// notebook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text
await page.locator('text=Remove').click();
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
// Click 'OK' on confirmation window and wait for save banner to appear
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
page.waitForSelector('.c-message-banner__message')
]);
@@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Click text=Ok
await Promise.all([
page.waitForNavigation(),
page.locator('text=Ok').click()
page.locator('button:has-text("OK")').click()
]);
// deleted page, should no longer exist
@@ -145,10 +145,9 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
test.beforeEach(async ({ page }) => {
const notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, notebook);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {

View File

@@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
createDomainObjectWithDefaults(page, { type: 'Notebook' });
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
// Create an entry
@@ -45,6 +45,8 @@ async function createNotebookAndEntry(page, iterations = 1) {
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
}
return notebook;
}
/**
@@ -53,7 +55,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, iterations);
const notebook = await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
@@ -75,6 +77,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
test.describe('Tagging in Notebooks @addInit', () => {
@@ -173,10 +177,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: 'Clock' });
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4;
await createNotebookEntryAndTags(page, ITERATIONS);
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@@ -189,11 +193,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Unnamed Clock
await page.click('text="Unnamed Clock"');
// Click Clock
await page.click(`text=${clock.name}`);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
// Click Notebook
await page.click(`text=${notebook.name}`);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@@ -207,14 +211,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.waitForLoadState('networkidle')
]);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
// Click Notebook
await page.click(`text="${notebook.name}"`);
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");
}
});
});

View File

@@ -110,10 +110,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
@@ -129,10 +129,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Overlay Plot")').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -106,7 +106,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2
@@ -123,7 +123,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -88,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Stacked Plot")').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -146,11 +146,11 @@ async function saveStackedPlot(page) {
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -68,10 +68,10 @@ async function makeOverlayPlot(page) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Overlay Plot")').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -86,13 +86,13 @@ async function makeOverlayPlot(page) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -25,8 +25,8 @@
*
*/
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults} = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
let sineWaveGeneratorObject;
@@ -40,7 +40,6 @@ test.describe('Plot Integrity Testing @unstable', () => {
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Capture the number of plots points and store as const name numberOfPlotPoints
//Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
@@ -51,4 +50,90 @@ test.describe('Plot Integrity Testing @unstable', () => {
});
expect(createMineFolderRequests.length).toEqual(0);
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
//Get pixel data from Canvas
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* This function edits a sine wave generator with the default options and enables the infinity values option.
*
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
*/
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
await page.goto(sineWaveGeneratorObject.url);
// Edit LAD table
await page.locator('[title="More options"]').click();
await page.locator('[title="Edit properties of this object."]').click();
// Modify the infinity option to true
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
await infinityInput.click();
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
// Thus, navigate away and back to the object.
await page.goto('./#/browse/mine');
await page.goto(sineWaveGeneratorObject.url);
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
state: 'hidden'
});
// FIXME: The progress bar disappears on series data load, not on plot render,
// so wait for a half a second before evaluating the canvas.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@@ -0,0 +1,93 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Scatter Plot component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
let scatterPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
});
test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Navigate to the scatter plot and verify that
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => {

View File

@@ -30,7 +30,7 @@ test.describe('Timer', () => {
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
test('Can perform actions on the Timer', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313'

View File

@@ -31,7 +31,7 @@ test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await createObjectsForSearch(page, myItemsFolderName);
const createdObjects = await createObjectsForSearch(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
@@ -41,8 +41,8 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
// Click the Elements pool to dismiss the search menu
await page.locator('.l-pane__label:has-text("Elements")').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
@@ -77,7 +77,7 @@ test.describe('Grand Search', () => {
await expect(page.locator('.is-object-type-clock')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
@@ -185,7 +185,7 @@ async function createFolderObject(page, folderName) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
// Create folder object
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
}
async function waitForSearchCompletion(page) {
@@ -197,75 +197,56 @@ async function waitForSearchCompletion(page) {
* Creates some domain objects for searching
* @param {import('@playwright/test').Page} page
*/
async function createObjectsForSearch(page, myItemsFolderName) {
async function createObjectsForSearch(page) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=1').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
page.locator('button:has-text("OK")').click()
]);
const redFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Red Folder'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=2').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const blueFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Blue Folder',
parent: redFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const clockA = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock A',
parent: blueFolder.uuid
});
const clockB = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock B',
parent: blueFolder.uuid
});
const clockC = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock C',
parent: blueFolder.uuid
});
const clockD = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock D',
parent: blueFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
return {
redFolder,
blueFolder,
clockA,
clockB,
clockC,
clockD,
displayLayout
};
}

View File

@@ -53,7 +53,7 @@ test.describe('Performance tests', () => {
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await page.locator('button:has-text("OK")').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();

View File

@@ -33,7 +33,8 @@ define([
dataRateInHz: 1,
randomness: 0,
phase: 0,
loadDelay: 0
loadDelay: 0,
infinityValues: false
};
function GeneratorProvider(openmct) {
@@ -56,7 +57,8 @@ define([
'dataRateInHz',
'randomness',
'phase',
'loadDelay'
'loadDelay',
'infinityValues'
];
request = request || {};

View File

@@ -76,10 +76,10 @@
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues)
}
});
nextStep += step;
@@ -117,6 +117,7 @@
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@@ -127,10 +128,10 @@
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
});
}
@@ -155,12 +156,20 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness) {
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
return amplitude
* Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function sin(timestamp, period, amplitude, offset, phase, randomness) {
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
return amplitude
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}

View File

@@ -143,6 +143,16 @@ define([
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
}
],
initialize: function (object) {
@@ -153,7 +163,8 @@ define([
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0
loadDelay: 0,
infinityValues: false
};
}
});

View File

@@ -30,8 +30,53 @@ if (document.currentScript) {
}
}
/**
* @typedef {object} BuildInfo
* @property {string} version
* @property {string} buildDate
* @property {string} revision
* @property {string} branch
*/
/**
* @typedef {object} OpenMCT
* @property {BuildInfo} buildInfo
* @property {*} selection
* @property {import('./src/api/time/TimeAPI').default} time
* @property {import('./src/api/composition/CompositionAPI').default} composition
* @property {*} objectViews
* @property {*} inspectorViews
* @property {*} propertyEditors
* @property {*} toolbars
* @property {*} types
* @property {import('./src/api/objects/ObjectAPI').default} objects
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
* @property {import('./src/api/user/UserAPI').default} user
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
* @property {import('./src/api/Editor').default} editor
* @property {import('./src/api/overlays/OverlayAPI')} overlays
* @property {import('./src/api/menu/MenuAPI').default} menus
* @property {import('./src/api/actions/ActionsAPI').default} actions
* @property {import('./src/api/status/StatusAPI').default} status
* @property {*} priority
* @property {import('./src/ui/router/ApplicationRouter')} router
* @property {import('./src/api/faultmanagement/FaultManagementAPI').default} faults
* @property {import('./src/api/forms/FormsAPI').default} forms
* @property {import('./src/api/Branding').default} branding
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
* @property {{(plugin: OpenMCTPlugin) => void}} install
* @property {{() => string}} getAssetPath
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
* @property {{() => void}} startHeadless
* @property {{() => void}} destroy
* @property {OpenMCTPlugin[]} plugins
* @property {OpenMCTComponent[]} components
*/
const MCT = require('./src/MCT');
/** @type {OpenMCT} */
const openmct = new MCT();
module.exports = openmct;

View File

@@ -1,19 +1,16 @@
{
"name": "openmct",
"version": "2.1.1",
"version": "2.1.5-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.10.3",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.16.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.25.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"babel-loader": "8.2.5",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
@@ -22,17 +19,17 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.23.1",
"eslint": "8.29.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.3.0",
"eslint-plugin-vue": "9.8.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "4.4.0",
"jasmine-core": "4.5.0",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
@@ -45,10 +42,10 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.1",
"mini-css-extract-plugin": "2.7.2",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.37",
"moment-timezone": "0.5.38",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.25.2",
@@ -56,17 +53,18 @@
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sass": "1.55.0",
"sass": "1.56.1",
"sass-loader": "13.0.2",
"sinon": "14.0.0",
"sinon": "14.0.1",
"style-loader": "^3.3.1",
"typescript": "4.9.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.74.0",
"webpack-cli": "4.10.0",
"webpack-cli": "5.0.0",
"webpack-dev-server": "4.11.1",
"webpack-merge": "5.8.0"
},
@@ -99,7 +97,7 @@
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod"
"prepare": "npm run build:prod && npx tsc"
},
"repository": {
"type": "git",
@@ -116,6 +114,5 @@
"ios_saf > 15"
],
"author": "",
"license": "Apache-2.0",
"private": true
"license": "Apache-2.0"
}

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* eslint-disable no-undef */
define([
'EventEmitter',
'./api/api',
@@ -81,13 +81,11 @@ define([
/**
* The Open MCT application. This may be configured by installing plugins
* or registering extensions before the application is started.
* @class MCT
* @constructor
* @memberof module:openmct
* @augments {EventEmitter}
*/
function MCT() {
EventEmitter.call(this);
/* eslint-disable no-undef */
this.buildInfo = {
version: __OPENMCT_VERSION__,
buildDate: __OPENMCT_BUILD_DATE__,
@@ -101,7 +99,7 @@ define([
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection(this)],
['selection', () => new Selection.default(this)],
/**
* MCT's time conductor, which may be used to synchronize view contents
@@ -125,7 +123,7 @@ define([
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI(this)],
['composition', () => new api.CompositionAPI.default(this)],
/**
* Registry for views of domain objects which should appear in the

View File

@@ -23,8 +23,7 @@
let brandingOptions = {};
/**
* @typedef {Object} BrandingOptions
* @memberOf openmct/branding
* @typedef {object} BrandingOptions
* @property {string} smallLogoImage URL to the image to use as the applications logo.
* This logo will appear on every screen and when clicked will launch the about dialog.
* @property {string} aboutHtml Custom content for the about screen. When defined the

View File

@@ -56,17 +56,12 @@ export default class Editor extends EventEmitter {
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
save() {
async save() {
const transaction = this.openmct.objects.getActiveTransaction();
return transaction.commit()
.then(() => {
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}).catch(error => {
throw error;
});
await transaction.commit();
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}
/**

View File

@@ -94,7 +94,6 @@ describe("The Annotation API", () => {
openmct.startHeadless();
});
afterEach(async () => {
openmct.objects.providers = {};
await resetApplicationState(openmct);
});
it("is defined", () => {

View File

@@ -37,7 +37,9 @@ define([
'./types/TypeRegistry',
'./user/UserAPI',
'./annotation/AnnotationAPI'
], function (
],
function (
ActionsAPI,
CompositionAPI,
EditorAPI,

View File

@@ -20,34 +20,41 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'EventEmitter',
'./DefaultCompositionProvider',
'./CompositionCollection'
], function (
_,
EventEmitter,
DefaultCompositionProvider,
CompositionCollection
) {
import DefaultCompositionProvider from './DefaultCompositionProvider';
import CompositionCollection from './CompositionCollection';
/**
* @typedef {import('./CompositionProvider').default} CompositionProvider
*/
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain objects
* it "contains" (for instance, that should be displayed beneath it
* in the tree.)
* @constructor
*/
export default class CompositionAPI {
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain objects
* it "contains" (for instance, that should be displayed beneath it
* in the tree.)
*
* @interface CompositionAPI
* @returns {module:openmct.CompositionCollection}
* @memberof module:openmct
* @param {OpenMCT} publicAPI
*/
function CompositionAPI(publicAPI) {
constructor(publicAPI) {
/** @type {CompositionProvider[]} */
this.registry = [];
/** @type {CompositionPolicy[]} */
this.policies = [];
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
/** @type {OpenMCT} */
this.publicAPI = publicAPI;
}
/**
* Add a composition provider.
*
@@ -55,21 +62,19 @@ define([
* behavior for certain domain objects.
*
* @method addProvider
* @param {module:openmct.CompositionProvider} provider the provider to add
* @memberof module:openmct.CompositionAPI#
* @param {CompositionProvider} provider the provider to add
*/
CompositionAPI.prototype.addProvider = function (provider) {
addProvider(provider) {
this.registry.unshift(provider);
};
}
/**
* Retrieve the composition (if any) of this domain object.
*
* @method get
* @returns {module:openmct.CompositionCollection}
* @memberof module:openmct.CompositionAPI#
* @param {DomainObject} domainObject
* @returns {CompositionCollection}
*/
CompositionAPI.prototype.get = function (domainObject) {
get(domainObject) {
const provider = this.registry.find(p => {
return p.appliesTo(domainObject);
});
@@ -79,8 +84,7 @@ define([
}
return new CompositionCollection(domainObject, provider, this.publicAPI);
};
}
/**
* A composition policy is a function which either allows or disallows
* placing one object in another's composition.
@@ -90,52 +94,51 @@ define([
* generally be written to return true in the default case.
*
* @callback CompositionPolicy
* @memberof module:openmct.CompositionAPI~
* @param {module:openmct.DomainObject} containingObject the object which
* @param {DomainObject} containingObject the object which
* would act as a container
* @param {module:openmct.DomainObject} containedObject the object which
* @param {DomainObject} containedObject the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
*/
/**
* Add a composition policy. Composition policies may disallow domain
* objects from containing other domain objects.
*
* @method addPolicy
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
* @param {CompositionPolicy} policy
* the policy to add
* @memberof module:openmct.CompositionAPI#
*/
CompositionAPI.prototype.addPolicy = function (policy) {
addPolicy(policy) {
this.policies.push(policy);
};
}
/**
* Check whether or not a domain object is allowed to contain another
* domain object.
*
* @private
* @method checkPolicy
* @param {module:openmct.DomainObject} containingObject the object which
* @param {DomainObject} container the object which
* would act as a container
* @param {module:openmct.DomainObject} containedObject the object which
* @param {DomainObject} containee the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
* @param {CompositionPolicy} policy
* the policy to add
* @memberof module:openmct.CompositionAPI#
*/
CompositionAPI.prototype.checkPolicy = function (container, containee) {
checkPolicy(container, containee) {
return this.policies.every(function (policy) {
return policy(container, containee);
});
};
}
CompositionAPI.prototype.supportsComposition = function (domainObject) {
/**
* Check whether or not a domainObject supports composition
*
* @param {DomainObject} domainObject
* @returns {boolean} true if the domainObject supports composition
*/
supportsComposition(domainObject) {
return this.get(domainObject) !== undefined;
};
}
}
return CompositionAPI;
});

View File

@@ -1,325 +1,319 @@
define([
'./CompositionAPI',
'./CompositionCollection'
], function (
CompositionAPI,
CompositionCollection
) {
import CompositionAPI from './CompositionAPI';
import CompositionCollection from './CompositionCollection';
describe('The Composition API', function () {
let publicAPI;
let compositionAPI;
let topicService;
let mutationTopic;
describe('The Composition API', function () {
let publicAPI;
let compositionAPI;
let topicService;
let mutationTopic;
beforeEach(function () {
mutationTopic = jasmine.createSpyObj('mutationTopic', [
'listen'
]);
topicService = jasmine.createSpy('topicService');
topicService.and.returnValue(mutationTopic);
publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get',
'mutate',
'observe',
'areIdsEqual'
]);
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
return id1.namespace === id2.namespace && id1.key === id2.key;
});
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
'checkPolicy'
]);
publicAPI.composition.checkPolicy.and.returnValue(true);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on'
]);
publicAPI.objects.get.and.callFake(function (identifier) {
return Promise.resolve({identifier: identifier});
});
publicAPI.$injector = jasmine.createSpyObj('$injector', [
'get'
]);
publicAPI.$injector.get.and.returnValue(topicService);
compositionAPI = new CompositionAPI(publicAPI);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
});
describe('default composition', function () {
let domainObject;
let composition;
beforeEach(function () {
mutationTopic = jasmine.createSpyObj('mutationTopic', [
'listen'
]);
topicService = jasmine.createSpy('topicService');
topicService.and.returnValue(mutationTopic);
publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get',
'mutate',
'observe',
'areIdsEqual'
]);
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
return id1.namespace === id2.namespace && id1.key === id2.key;
});
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
'checkPolicy'
]);
publicAPI.composition.checkPolicy.and.returnValue(true);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on'
]);
publicAPI.objects.get.and.callFake(function (identifier) {
return Promise.resolve({identifier: identifier});
});
publicAPI.$injector = jasmine.createSpyObj('$injector', [
'get'
]);
publicAPI.$injector.get.and.returnValue(topicService);
compositionAPI = new CompositionAPI(publicAPI);
domainObject = {
name: 'test folder',
identifier: {
namespace: 'test',
key: '1'
},
composition: [
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
describe('default composition', function () {
let domainObject;
let composition;
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
beforeEach(function () {
domainObject = {
name: 'test folder',
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {
namespace: 'test',
key: '1'
},
composition: [
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {
namespace: 'test',
key: 'a'
}
});
key: 'a'
}
});
});
describe('supports reordering of composition', function () {
let listener;
beforeEach(function () {
listener = jasmine.createSpy('reorderListener');
composition.on('reorder', listener);
});
describe('supports reordering of composition', function () {
let listener;
beforeEach(function () {
listener = jasmine.createSpy('reorderListener');
composition.on('reorder', listener);
return composition.load();
});
it('', function () {
composition.reorder(1, 0);
let newComposition =
return composition.load();
});
it('', function () {
composition.reorder(1, 0);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(1);
expect(reorderPlan.newIndex).toBe(0);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a');
expect(newComposition[2].key).toEqual('c');
});
it('', function () {
composition.reorder(0, 2);
let newComposition =
expect(reorderPlan.oldIndex).toBe(1);
expect(reorderPlan.newIndex).toBe(0);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a');
expect(newComposition[2].key).toEqual('c');
});
it('', function () {
composition.reorder(0, 2);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(0);
expect(reorderPlan.newIndex).toBe(2);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('c');
expect(newComposition[2].key).toEqual('a');
expect(reorderPlan.oldIndex).toBe(0);
expect(reorderPlan.newIndex).toBe(2);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('c');
expect(newComposition[2].key).toEqual('a');
});
});
it('supports adding an object to composition', function () {
let addListener = jasmine.createSpy('addListener');
let mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.on('add', addListener);
composition.add(mockChildObject);
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
composition.on('add', addListener);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
expect(addListener).toHaveBeenCalled();
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
it('supports adding an object to composition', function () {
let addListener = jasmine.createSpy('addListener');
let mockChildObject = {
});
describe('Calling add or remove', function () {
let mockChildObject;
beforeEach(function () {
mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.on('add', addListener);
composition.add(mockChildObject);
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
composition.on('add', addListener);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
expect(addListener).toHaveBeenCalled();
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
describe('Calling add or remove', function () {
let mockChildObject;
beforeEach(function () {
mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.add(mockChildObject);
});
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
});
});
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
const removeListener = jasmine.createSpy('removeListener');
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
return composition.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
return addPromise;
}).then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
return removePromise;
}).then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
});
});
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
const removeListener = jasmine.createSpy('removeListener');
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
return composition.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
return addPromise;
}).then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
return removePromise;
}).then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
});
});

View File

@@ -20,75 +20,98 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash'
], function (
_
) {
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* @typedef {object} ListenerMap
* @property {Array.<any>} add
* @property {Array.<any>} remove
* @property {Array.<any>} load
* @property {Array.<any>} reorder
*/
/**
* A CompositionCollection represents the list of domain objects contained
* by another domain object. It provides methods for loading this
* list asynchronously, modifying this list, and listening for changes to
* this list.
*
* Usage:
* ```javascript
* var myViewComposition = MCT.composition.get(myViewObject);
* myViewComposition.on('add', addObjectToView);
* myViewComposition.on('remove', removeObjectFromView);
* myViewComposition.load(); // will trigger `add` for all loaded objects.
* ```
*/
export default class CompositionCollection {
domainObject;
#provider;
#publicAPI;
#listeners;
#mutables;
/**
* A CompositionCollection represents the list of domain objects contained
* by another domain object. It provides methods for loading this
* list asynchronously, modifying this list, and listening for changes to
* this list.
*
* Usage:
* ```javascript
* var myViewComposition = MCT.composition.get(myViewObject);
* myViewComposition.on('add', addObjectToView);
* myViewComposition.on('remove', removeObjectFromView);
* myViewComposition.load(); // will trigger `add` for all loaded objects.
* ```
*
* @interface CompositionCollection
* @param {module:openmct.DomainObject} domainObject the domain object
* @constructor
* @param {DomainObject} domainObject the domain object
* whose composition will be contained
* @param {module:openmct.CompositionProvider} provider the provider
* @param {import('./CompositionProvider').default} provider the provider
* to use to retrieve other domain objects
* @param {module:openmct.CompositionAPI} api the composition API, for
* @param {OpenMCT} publicAPI the composition API, for
* policy checks
* @memberof module:openmct
*/
function CompositionCollection(domainObject, provider, publicAPI) {
constructor(domainObject, provider, publicAPI) {
this.domainObject = domainObject;
this.provider = provider;
this.publicAPI = publicAPI;
this.listeners = {
/** @type {import('./CompositionProvider').default} */
this.#provider = provider;
/** @type {OpenMCT} */
this.#publicAPI = publicAPI;
/** @type {ListenerMap} */
this.#listeners = {
add: [],
remove: [],
load: [],
reorder: []
};
this.onProviderAdd = this.onProviderAdd.bind(this);
this.onProviderRemove = this.onProviderRemove.bind(this);
this.mutables = {};
this.onProviderAdd = this.#onProviderAdd.bind(this);
this.onProviderRemove = this.#onProviderRemove.bind(this);
this.#mutables = {};
if (this.domainObject.isMutable) {
this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
Object.values(this.#mutables).forEach(mutable => {
this.#publicAPI.objects.destroyMutable(mutable);
});
unobserve();
});
}
}
/**
* Listen for changes to this composition. Supports 'add', 'remove', and
* 'load' events.
*
* @param event event to listen for, either 'add', 'remove' or 'load'.
* @param callback to trigger when event occurs.
* @param [context] context to use when invoking callback, optional.
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
* @param {(...args: any[]) => void} callback to trigger when event occurs.
* @param {any} [context] to use when invoking callback, optional.
*/
CompositionCollection.prototype.on = function (event, callback, context) {
if (!this.listeners[event]) {
on(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
if (this.provider.on && this.provider.off) {
if (this.#provider.on && this.#provider.off) {
if (event === 'add') {
this.provider.on(
this.#provider.on(
this.domainObject,
'add',
this.onProviderAdd,
@@ -97,7 +120,7 @@ define([
}
if (event === 'remove') {
this.provider.on(
this.#provider.on(
this.domainObject,
'remove',
this.onProviderRemove,
@@ -106,36 +129,34 @@ define([
}
if (event === 'reorder') {
this.provider.on(
this.#provider.on(
this.domainObject,
'reorder',
this.onProviderReorder,
this.#onProviderReorder,
this
);
}
}
this.listeners[event].push({
this.#listeners[event].push({
callback: callback,
context: context
});
};
}
/**
* Remove a listener. Must be called with same exact parameters as
* `off`.
*
* @param event
* @param callback
* @param [context]
* @param {string} event
* @param {(...args: any[]) => void} callback
* @param {any} [context]
*/
CompositionCollection.prototype.off = function (event, callback, context) {
if (!this.listeners[event]) {
off(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
const index = this.listeners[event].findIndex(l => {
const index = this.#listeners[event].findIndex(l => {
return l.callback === callback && l.context === context;
});
@@ -143,125 +164,116 @@ define([
throw new Error('Tried to remove a listener that does not exist');
}
this.listeners[event].splice(index, 1);
if (this.listeners[event].length === 0) {
this.#listeners[event].splice(index, 1);
if (this.#listeners[event].length === 0) {
this._destroy();
// Remove provider listener if this is the last callback to
// be removed.
if (this.provider.off && this.provider.on) {
if (this.#provider.off && this.#provider.on) {
if (event === 'add') {
this.provider.off(
this.#provider.off(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} else if (event === 'remove') {
this.provider.off(
this.#provider.off(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
} else if (event === 'reorder') {
this.provider.off(
this.#provider.off(
this.domainObject,
'reorder',
this.onProviderReorder,
this.#onProviderReorder,
this
);
}
}
}
};
}
/**
* Add a domain object to this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* @param {module:openmct.DomainObject} child the domain object to add
* @param {boolean} skipMutate true if the underlying provider should
* not be updated
* @memberof module:openmct.CompositionCollection#
* @name add
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to add
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
*/
CompositionCollection.prototype.add = function (child, skipMutate) {
add(child, skipMutate) {
if (!skipMutate) {
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
this.provider.add(this.domainObject, child.identifier);
this.#provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.publicAPI.objects.toMutable(child);
this.mutables[keyString] = child;
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
this.emit('add', child);
this.#emit('add', child);
}
};
}
/**
* Load the domain objects in this composition.
*
* @returns {Promise.<Array.<module:openmct.DomainObject>>} a promise for
* @param {AbortSignal} abortSignal
* @returns {Promise.<Array.<DomainObject>>} a promise for
* the domain objects in this composition
* @memberof {module:openmct.CompositionCollection#}
* @name load
*/
CompositionCollection.prototype.load = function (abortSignal) {
this.cleanUpMutables();
return this.provider.load(this.domainObject)
.then(function (children) {
return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal)));
}.bind(this))
.then(function (childObjects) {
childObjects.forEach(c => this.add(c, true));
return childObjects;
}.bind(this))
.then(function (children) {
this.emit('load');
return children;
}.bind(this));
};
async load(abortSignal) {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
childObjects.forEach(c => this.add(c, true));
this.#emit('load');
return childObjects;
}
/**
* Remove a domain object from this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* @param {module:openmct.DomainObject} child the domain object to remove
* @param {boolean} skipMutate true if the underlying provider should
* not be updated
* @memberof module:openmct.CompositionCollection#
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to remove
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
* @name remove
*/
CompositionCollection.prototype.remove = function (child, skipMutate) {
remove(child, skipMutate) {
if (!skipMutate) {
this.provider.remove(this.domainObject, child.identifier);
this.#provider.remove(this.domainObject, child.identifier);
} else {
if (this.returnMutables) {
let keyString = this.publicAPI.objects.makeKeyString(child);
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
delete this.mutables[keyString];
let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
}
}
this.emit('remove', child);
this.#emit('remove', child);
}
};
}
/**
* Reorder the domain objects in this composition.
*
@@ -270,67 +282,75 @@ define([
*
* @param {number} oldIndex
* @param {number} newIndex
* @memberof module:openmct.CompositionCollection#
* @name remove
*/
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) {
this.provider.reorder(this.domainObject, oldIndex, newIndex);
};
reorder(oldIndex, newIndex, _skipMutate) {
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
}
/**
* Handle reorder from provider.
* @private
* Destroy mutationListener
*/
CompositionCollection.prototype.onProviderReorder = function (reorderMap) {
this.emit('reorder', reorderMap);
};
/**
* Handle adds from provider.
* @private
*/
CompositionCollection.prototype.onProviderAdd = function (childId) {
return this.publicAPI.objects.get(childId).then(function (child) {
this.add(child, true);
return child;
}.bind(this));
};
/**
* Handle removal from provider.
* @private
*/
CompositionCollection.prototype.onProviderRemove = function (child) {
this.remove(child, true);
};
CompositionCollection.prototype._destroy = function () {
_destroy() {
if (this.mutationListener) {
this.mutationListener();
delete this.mutationListener;
}
};
}
/**
* Handle reorder from provider.
* @private
* @param {object} reorderMap
*/
#onProviderReorder(reorderMap) {
this.#emit('reorder', reorderMap);
}
/**
* Handle adds from provider.
* @private
* @param {import('../objects/ObjectAPI').Identifier} childId
* @returns {DomainObject}
*/
#onProviderAdd(childId) {
return this.#publicAPI.objects.get(childId).then(function (child) {
this.add(child, true);
return child;
}.bind(this));
}
/**
* Handle removal from provider.
* @param {DomainObject} child
*/
#onProviderRemove(child) {
this.remove(child, true);
}
/**
* Emit events.
*
* @private
* @param {string} event
* @param {...args.<any>} payload
*/
CompositionCollection.prototype.emit = function (event, ...payload) {
this.listeners[event].forEach(function (l) {
#emit(event, ...payload) {
this.#listeners[event].forEach(function (l) {
if (l.context) {
l.callback.apply(l.context, payload);
} else {
l.callback(...payload);
}
});
};
}
CompositionCollection.prototype.cleanUpMutables = function () {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
/**
* Destroy all mutables.
* @private
*/
#cleanUpMutables() {
Object.values(this.#mutables).forEach(mutable => {
this.#publicAPI.objects.destroyMutable(mutable);
});
};
return CompositionCollection;
});
}
}

View File

@@ -0,0 +1,262 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import objectUtils from "../objects/object-utils";
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
/**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
*/
export default class CompositionProvider {
#publicAPI;
#listeningTo;
/**
* @param {OpenMCT} publicAPI
* @param {CompositionAPI} compositionAPI
*/
constructor(publicAPI, compositionAPI) {
this.#publicAPI = publicAPI;
this.#listeningTo = {};
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
}
get listeningTo() {
return this.#listeningTo;
}
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
get publicAPI() {
return this.#publicAPI;
}
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @method appliesTo
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @method load
*/
load(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
}
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}
/**
* @private
* @param {DomainObject} parent
* @param {DomainObject} child
* @returns {boolean}
*/
#cannotContainItself(parent, child) {
return !(parent.identifier.namespace === child.identifier.namespace
&& parent.identifier.key === child.identifier.key);
}
/**
* @private
* @param {DomainObject} parent
* @returns {boolean}
*/
#supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent);
}
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
}

View File

@@ -19,102 +19,79 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from "../objects/object-utils";
import CompositionProvider from './CompositionProvider';
define([
'lodash',
'objectUtils'
], function (
_,
objectUtils
) {
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
* @interface CompositionProvider
* @memberof module:openmct
*/
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
function DefaultCompositionProvider(publicAPI, compositionAPI) {
this.publicAPI = publicAPI;
this.listeningTo = {};
this.onMutation = this.onMutation.bind(this);
/**
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
this.cannotContainItself = this.cannotContainItself.bind(this);
this.supportsComposition = this.supportsComposition.bind(this);
/**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
compositionAPI.addPolicy(this.cannotContainItself);
compositionAPI.addPolicy(this.supportsComposition);
}
/**
* @private
*/
DefaultCompositionProvider.prototype.cannotContainItself = function (parent, child) {
return !(parent.identifier.namespace === child.identifier.namespace
&& parent.identifier.key === child.identifier.key);
};
/**
* @private
*/
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
return this.publicAPI.composition.supportsComposition(parent);
};
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
* @extends CompositionProvider
*/
export default class DefaultCompositionProvider extends CompositionProvider {
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @param {module:openmct.DomainObject} domainObject the domain object
* @override
* @param {DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide
* composition for a given domain object
* @memberof module:openmct.CompositionProvider#
* @method appliesTo
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) {
appliesTo(domainObject) {
return Boolean(domainObject.composition);
};
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @param {module:openmct.DomainObject} domainObject the domain object
* @override
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise.<Array.<module:openmct.Identifier>>} a promise for
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @memberof module:openmct.CompositionProvider#
* @method load
*/
DefaultCompositionProvider.prototype.load = function (domainObject) {
load(domainObject) {
return Promise.all(domainObject.composition);
};
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @param {module:openmct.DomainObject} domainObject to listen to
* @param String event the event to bind to, either `add` or `remove`.
* @param Function callback callback to invoke when event is triggered.
* @param [context] context to use when invoking callback.
* @override
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
DefaultCompositionProvider.prototype.on = function (
domainObject,
on(domainObject,
event,
callback,
context
) {
context) {
this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
@@ -131,24 +108,24 @@ define([
callback: callback,
context: context
});
};
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @param {module:openmct.DomainObject} domainObject to remove listener for
* @param String event event to stop listening to: `add` or `remove`.
* @param Function callback callback to remove.
* @param [context] context of callback to remove.
* @override
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
DefaultCompositionProvider.prototype.off = function (
domainObject,
off(domainObject,
event,
callback,
context
) {
context) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
@@ -160,57 +137,64 @@ define([
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
delete this.listeningTo[keyString];
}
};
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {module:openmct.DomainObject} domainObject the domain object
* @override
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {module:openmct.DomainObject} child the domain object to remove
* @memberof module:openmct.CompositionProvider#
* @param {Identifier} childId the domain object to remove
* @method remove
*/
DefaultCompositionProvider.prototype.remove = function (domainObject, childId) {
remove(domainObject, childId) {
let composition = domainObject.composition.filter(function (child) {
return !(childId.namespace === child.namespace
&& childId.key === child.key);
&& childId.key === child.key);
});
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
};
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {module:openmct.DomainObject} domainObject the domain object
* @override
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {module:openmct.DomainObject} child the domain object to add
* @memberof module:openmct.CompositionProvider#
* @param {Identifier} childId the domain object to add
* @method add
*/
DefaultCompositionProvider.prototype.add = function (parent, childId) {
add(parent, childId) {
if (!this.includes(parent, childId)) {
parent.composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
}
};
}
/**
* @private
* @override
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
return parent.composition.some(composee =>
this.publicAPI.objects.areIdsEqual(composee, childId));
};
includes(parent, childId) {
return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
}
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {
/**
* @override
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
let newComposition = domainObject.composition.slice();
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
@@ -241,6 +225,7 @@ define([
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
@@ -257,66 +242,5 @@ define([
listener.callback(reorderPlan);
}
}
};
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
*
* @private
*/
DefaultCompositionProvider.prototype.establishTopicListener = function () {
if (this.topicListener) {
return;
}
this.publicAPI.objects.eventEmitter.on('mutation', this.onMutation);
this.topicListener = () => {
this.publicAPI.objects.eventEmitter.off('mutation', this.onMutation);
};
};
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
*/
DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
};
return DefaultCompositionProvider;
});
}
}

View File

@@ -23,13 +23,11 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
import EventEmitter from 'EventEmitter';
import Vue from 'vue';
import _ from 'lodash';
export default class FormsAPI extends EventEmitter {
export default class FormsAPI {
constructor(openmct) {
super();
this.openmct = openmct;
this.formController = new FormController(openmct);
}
@@ -92,29 +90,75 @@ export default class FormsAPI extends EventEmitter {
/**
* Show form inside an Overlay dialog with given form structure
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {function} onChange a callback function when any changes detected
*/
showForm(formStructure, {
onChange
} = {}) {
let overlay;
const self = this;
const overlayEl = document.createElement('div');
overlayEl.classList.add('u-contents');
overlay = self.openmct.overlays.overlay({
element: overlayEl,
size: 'dialog'
});
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = resolve;
formCancel = reject;
});
this.showCustomForm(formStructure, {
element: overlayEl,
onChange
})
.then((response) => {
overlay.dismiss();
formSave(response);
})
.catch((response) => {
overlay.dismiss();
formCancel(response);
});
return promise;
}
/**
* Show form as a child of the element provided with given form structure
*
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {HTMLElement} element Parent Element to render a Form
* @property {function} onChange a callback function when any changes detected
* @property {function} onSave a callback function when form is submitted
* @property {function} onDismiss a callback function when form is dismissed
*/
showForm(formStructure, {
showCustomForm(formStructure, {
element,
onChange
} = {}) {
const changes = {};
let overlay;
let onDismiss;
let onSave;
if (element === undefined) {
throw Error('Required element parameter not provided');
}
const self = this;
const changes = {};
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
onSave = onFormAction(resolve);
onDismiss = onFormAction(reject);
formSave = onFormAction(resolve);
formCancel = onFormAction(reject);
});
const vm = new Vue({
@@ -126,26 +170,17 @@ export default class FormsAPI extends EventEmitter {
return {
formStructure,
onChange: onFormPropertyChange,
onDismiss,
onSave
onCancel: formCancel,
onSave: formSave
};
},
template: '<FormProperties :model="formStructure" @onChange="onChange" @onDismiss="onDismiss" @onSave="onSave"></FormProperties>'
template: '<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
}).$mount();
const formElement = vm.$el;
if (element) {
element.append(formElement);
} else {
overlay = self.openmct.overlays.overlay({
element: vm.$el,
size: 'dialog',
onDestroy: () => vm.$destroy()
});
}
element.append(formElement);
function onFormPropertyChange(data) {
self.emit('onFormPropertyChange', data);
if (onChange) {
onChange(data);
}
@@ -158,17 +193,14 @@ export default class FormsAPI extends EventEmitter {
key = property.join('.');
}
changes[key] = data.value;
_.set(changes, key, data.value);
}
}
function onFormAction(callback) {
return () => {
if (element) {
formElement.remove();
} else {
overlay.dismiss();
}
formElement.remove();
vm.$destroy();
if (callback) {
callback(changes);

View File

@@ -133,7 +133,7 @@ describe('The Forms API', () => {
});
it('when container element is provided', (done) => {
openmct.forms.showForm(formStructure, { element }).catch(() => {
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');

View File

@@ -73,7 +73,7 @@
tabindex="0"
class="c-button js-cancel-button"
aria-label="Cancel"
@click="onDismiss"
@click="onCancel"
>
{{ cancelLabel }}
</button>
@@ -164,8 +164,8 @@ export default {
this.$emit('onChange', data);
},
onDismiss() {
this.$emit('onDismiss');
onCancel() {
this.$emit('onCancel');
},
onSave() {
this.$emit('onSave');

View File

@@ -26,6 +26,7 @@
v-model="selected"
required="model.required"
name="mctControl"
:aria-label="model.ariaLabel || model.name"
@change="onChange($event)"
>
<option

View File

@@ -27,6 +27,7 @@
:class="model.cssClass"
>
<textarea
:id="`${model.key}-textarea`"
v-model="field"
type="text"
:size="model.size"

View File

@@ -29,6 +29,7 @@
<ToggleSwitch
id="switchId"
:checked="isChecked"
:name="model.name"
@change="toggleCheckBox"
/>
</span>

View File

@@ -3,39 +3,52 @@
class="c-menu"
:class="options.menuClass"
>
<ul v-if="options.actions.length && options.actions[0].length">
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
role="group"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
</ul>
<ul v-else>
<ul
v-else
role="menu"
>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"

View File

@@ -5,45 +5,54 @@
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
role="group"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
</ul>
<ul
v-else
class="c-super-menu__menu"
role="menu"
>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"

View File

@@ -19,6 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const DEFAULT_INTERCEPTOR_PRIORITY = 0;
export default class InterceptorRegistry {
/**
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
@@ -45,7 +46,6 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
@@ -56,10 +56,18 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
getInterceptors(identifier, object) {
function byPriority(interceptorA, interceptorB) {
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
return priorityB - priorityA;
}
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, object);
});
}).sort(byPriority);
}
}

View File

@@ -75,11 +75,7 @@ class MutableDomainObject {
return eventOff;
}
$set(path, value) {
_.set(this, path, value);
if (path !== 'persisted' && path !== 'modified') {
_.set(this, 'modified', Date.now());
}
MutableDomainObject.mutateObject(this, path, value);
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
@@ -136,8 +132,11 @@ class MutableDomainObject {
}
static mutateObject(object, path, value) {
if (path !== 'persisted') {
_.set(object, 'modified', Date.now());
}
_.set(object, path, value);
_.set(object, 'modified', Date.now());
}
}

View File

@@ -33,7 +33,7 @@ import InMemorySearchProvider from './InMemorySearchProvider';
/**
* Uniquely identifies a domain object.
*
* @typedef Identifier
* @typedef {object} Identifier
* @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored.
* @property {string} key a unique identifier for the domain object
@@ -50,8 +50,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit.
*
* @typedef DomainObject
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
* @typedef {object} DomainObject
* @property {Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object
* @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object
@@ -59,19 +59,19 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* object
* @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
* @property {Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @memberof module:openmct
* @memberof module:openmct.ObjectAPI~
*/
/**
* @readonly
* @enum {String} SEARCH_TYPES
* @property {String} OBJECTS Search for objects
* @property {String} ANNOTATIONS Search for annotations
* @property {String} TAGS Search for tags
*/
* @readonly
* @enum {string} SEARCH_TYPES
* @property {string} OBJECTS Search for objects
* @property {string} ANNOTATIONS Search for annotations
* @property {string} TAGS Search for tags
*/
/**
* Utilities for loading, saving, and manipulating domain objects.
@@ -96,7 +96,7 @@ export default class ObjectAPI {
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
this.errors = {
Conflict: ConflictError
@@ -204,13 +204,13 @@ export default class ObjectAPI {
}
identifier = utils.parseKeyString(identifier);
let dirtyObject;
if (this.isTransactionActive()) {
dirtyObject = this.transaction.getDirtyObject(identifier);
}
if (dirtyObject) {
return Promise.resolve(dirtyObject);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
const provider = this.getProvider(identifier);
@@ -354,39 +354,59 @@ export default class ObjectAPI {
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
save(domainObject) {
let provider = this.getProvider(domainObject.identifier);
let savedResolve;
let savedReject;
async save(domainObject) {
const provider = this.getProvider(domainObject.identifier);
let result;
let lastPersistedTime;
if (!this.isPersistable(domainObject.identifier)) {
result = Promise.reject('Object provider does not support saving');
} else if (this.#hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true);
} else {
const persistedTime = Date.now();
if (domainObject.persisted === undefined) {
result = new Promise((resolve, reject) => {
savedResolve = resolve;
savedReject = reject;
});
domainObject.persisted = persistedTime;
const newObjectPromise = provider.create(domainObject);
if (newObjectPromise) {
newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
}).catch((error) => {
savedReject(error);
});
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
}
const username = await this.#getCurrentUsername();
const isNewObject = domainObject.persisted === undefined;
let savedResolve;
let savedReject;
let savedObjectPromise;
result = new Promise((resolve, reject) => {
savedResolve = resolve;
savedReject = reject;
});
this.#mutate(domainObject, 'modifiedBy', username);
if (isNewObject) {
this.#mutate(domainObject, 'createdBy', username);
const createdTime = Date.now();
this.#mutate(domainObject, 'created', createdTime);
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.create(domainObject);
} else {
domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime);
result = provider.update(domainObject);
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
if (savedObjectPromise) {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
savedReject(error);
});
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`);
}
}
@@ -399,8 +419,21 @@ export default class ObjectAPI {
});
}
async #getCurrentUsername() {
const user = await this.openmct.user.getCurrentUser();
let username;
if (user !== undefined) {
username = user.getName();
}
return username;
}
/**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*
* @returns {Transaction} a new Transaction that was just created
*/
startTransaction() {
if (this.isTransactionActive()) {
@@ -408,6 +441,8 @@ export default class ObjectAPI {
}
this.transaction = new Transaction(this);
return this.transaction;
}
/**
@@ -480,14 +515,16 @@ export default class ObjectAPI {
}
/**
* Modify a domain object.
* Modify a domain object. Internal to ObjectAPI, won't call save after.
* @private
*
* @param {module:openmct.DomainObject} object the object to mutate
* @param {string} path the property to modify
* @param {*} value the new value for this property
* @method mutate
* @memberof module:openmct.ObjectAPI#
*/
mutate(domainObject, path, value) {
#mutate(domainObject, path, value) {
if (!this.supportsMutation(domainObject.identifier)) {
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
}
@@ -508,6 +545,18 @@ export default class ObjectAPI {
//Destroy temporary mutable object
this.destroyMutable(mutableDomainObject);
}
}
/**
* Modify a domain object and save.
* @param {module:openmct.DomainObject} object the object to mutate
* @param {string} path the property to modify
* @param {*} value the new value for this property
* @method mutate
* @memberof module:openmct.ObjectAPI#
*/
mutate(domainObject, path, value) {
this.#mutate(domainObject, path, value);
if (this.isTransactionActive()) {
this.transaction.add(domainObject);
@@ -684,7 +733,7 @@ export default class ObjectAPI {
}
isTransactionActive() {
return Boolean(this.transaction && this.openmct.editor.isEditing());
return this.transaction !== undefined && this.transaction !== null;
}
#hasAlreadyBeenPersisted(domainObject) {

View File

@@ -8,13 +8,27 @@ describe("The Object API", () => {
let mockDomainObject;
const TEST_NAMESPACE = "test-namespace";
const TEST_KEY = "test-key";
const USERNAME = 'Joan Q Public';
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach((done) => {
typeRegistry = jasmine.createSpyObj('typeRegistry', [
'get'
]);
const userProvider = {
isLoggedIn() {
return true;
},
getCurrentUser() {
return Promise.resolve({
getName() {
return USERNAME;
}
});
}
};
openmct = createOpenMct();
openmct.user.setProvider(userProvider);
objectAPI = openmct.objects;
openmct.editor = {};
@@ -63,19 +77,63 @@ describe("The Object API", () => {
mockProvider.update.and.returnValue(Promise.resolve(true));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
});
it("Calls 'create' on provider if object is new", () => {
objectAPI.save(mockDomainObject);
it("Adds a 'created' timestamp to new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.created).not.toBeUndefined();
});
it("Calls 'create' on provider if object is new", async () => {
await objectAPI.save(mockDomainObject);
expect(mockProvider.create).toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
it("Calls 'update' on provider if object is not new", () => {
it("Calls 'update' on provider if object is not new", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
objectAPI.save(mockDomainObject);
await objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).toHaveBeenCalled();
});
describe("the persisted timestamp for existing objects", () => {
let persistedTimestamp;
beforeEach(() => {
persistedTimestamp = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.persisted = persistedTimestamp;
mockDomainObject.modified = Date.now();
});
it("is updated", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
expect(mockDomainObject.persisted > persistedTimestamp).toBe(true);
});
it("is >= modified timestamp", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
describe("the persisted timestamp for new objects", () => {
it("is updated", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
});
it("is >= modified timestamp", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
it("Sets the current user for 'createdBy' on new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.createdBy).toBe(USERNAME);
});
it("Sets the current user for 'modifedBy' on existing objects", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
});
it("Does not persist if the object is unchanged", () => {
mockDomainObject.persisted =

View File

@@ -17,6 +17,7 @@ class Overlay extends EventEmitter {
dismissable = true,
element,
onDestroy,
onDismiss,
size
} = {}) {
super();
@@ -32,7 +33,7 @@ class Overlay extends EventEmitter {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.dismiss.bind(this),
dismiss: this.notifyAndDismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
@@ -43,6 +44,10 @@ class Overlay extends EventEmitter {
if (onDestroy) {
this.once('destroy', onDestroy);
}
if (onDismiss) {
this.once('dismiss', onDismiss);
}
}
dismiss() {
@@ -51,6 +56,12 @@ class Overlay extends EventEmitter {
this.component.$destroy();
}
//Ensures that any callers are notified that the overlay is dismissed
notifyAndDismiss() {
this.emit('dismiss');
this.dismiss();
}
/**
* @private
**/

View File

@@ -55,7 +55,7 @@ class OverlayAPI {
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
lastOverlay.notifyAndDismiss();
}
}

View File

@@ -27,7 +27,6 @@ import TelemetryMetadataManager from './TelemetryMetadataManager';
import TelemetryValueFormatter from './TelemetryValueFormatter';
import DefaultMetadataProvider from './DefaultMetadataProvider';
import objectUtils from 'objectUtils';
import _ from 'lodash';
export default class TelemetryAPI {
@@ -73,7 +72,7 @@ export default class TelemetryAPI {
* @returns {boolean} true if the object is a telemetry object.
*/
isTelemetryObject(domainObject) {
return Boolean(this.findMetadataProvider(domainObject));
return Boolean(this.#findMetadataProvider(domainObject));
}
/**
@@ -87,7 +86,7 @@ export default class TelemetryAPI {
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
canProvideTelemetry(domainObject) {
return Boolean(this.findSubscriptionProvider(domainObject))
return Boolean(this.#findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject));
}
@@ -120,7 +119,7 @@ export default class TelemetryAPI {
/**
* @private
*/
findSubscriptionProvider() {
#findSubscriptionProvider() {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsSubscribe.apply(provider, args);
@@ -130,9 +129,10 @@ export default class TelemetryAPI {
}
/**
* @private
* Returns a telemetry request provider that supports
* a given domain object and options.
*/
findRequestProvider(domainObject) {
findRequestProvider() {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsRequest.apply(provider, args);
@@ -144,7 +144,7 @@ export default class TelemetryAPI {
/**
* @private
*/
findMetadataProvider(domainObject) {
#findMetadataProvider(domainObject) {
return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject);
})[0];
@@ -153,7 +153,7 @@ export default class TelemetryAPI {
/**
* @private
*/
findLimitEvaluator(domainObject) {
#findLimitEvaluator(domainObject) {
return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject);
})[0];
@@ -161,6 +161,7 @@ export default class TelemetryAPI {
/**
* @private
* Though used in TelemetryCollection as well
*/
standardizeRequestOptions(options) {
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
@@ -174,6 +175,10 @@ export default class TelemetryAPI {
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key;
}
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
}
/**
@@ -241,7 +246,7 @@ export default class TelemetryAPI {
/**
* Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters
* (start, end, etc.), sort order, and strategies for retrieving
* (start, end, etc.), sort order, time context, and strategies for retrieving
* telemetry (aggregation, latest available, etc.).
*
* @method request
@@ -255,7 +260,7 @@ export default class TelemetryAPI {
*/
async request(domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
return [];
}
if (arguments.length === 1) {
@@ -273,22 +278,24 @@ export default class TelemetryAPI {
if (!provider) {
this.requestAbortControllers.delete(abortController);
return this.handleMissingRequestProvider(domainObject);
return this.#handleMissingRequestProvider(domainObject);
}
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
try {
const telemetry = await provider.request(...arguments);
return provider.request.apply(provider, arguments)
.catch((rejected) => {
if (rejected.name !== 'AbortError') {
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
console.error(rejected);
}
return telemetry;
} catch (error) {
if (error.name !== 'AbortError') {
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
console.error(error);
}
return Promise.reject(rejected);
}).finally(() => {
this.requestAbortControllers.delete(abortController);
});
throw new Error(error);
} finally {
this.requestAbortControllers.delete(abortController);
}
}
/**
@@ -306,7 +313,7 @@ export default class TelemetryAPI {
* the subscription
*/
subscribe(domainObject, callback, options) {
const provider = this.findSubscriptionProvider(domainObject);
const provider = this.#findSubscriptionProvider(domainObject);
if (!this.subscribeCache) {
this.subscribeCache = {};
@@ -353,7 +360,7 @@ export default class TelemetryAPI {
*/
getMetadata(domainObject) {
if (!this.metadataCache.has(domainObject)) {
const metadataProvider = this.findMetadataProvider(domainObject);
const metadataProvider = this.#findMetadataProvider(domainObject);
if (!metadataProvider) {
return;
}
@@ -369,33 +376,6 @@ export default class TelemetryAPI {
return this.metadataCache.get(domainObject);
}
/**
* Return an array of valueMetadatas that are common to all supplied
* telemetry objects and match the requested hints.
*
*/
commonValuesForHints(metadatas, hints) {
const options = metadatas.map(function (metadata) {
const values = metadata.valuesForHints(hints);
return _.keyBy(values, 'key');
}).reduce(function (a, b) {
const results = {};
Object.keys(a).forEach(function (key) {
if (Object.prototype.hasOwnProperty.call(b, key)) {
results[key] = a[key];
}
});
return results;
});
const sortKeys = hints.map(function (h) {
return 'hints.' + h;
});
return _.sortBy(options, sortKeys);
}
/**
* Get a value formatter for a given valueMetadata.
*
@@ -450,7 +430,7 @@ export default class TelemetryAPI {
*
* @returns Promise
*/
handleMissingRequestProvider(domainObject) {
#handleMissingRequestProvider(domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
@@ -540,7 +520,7 @@ export default class TelemetryAPI {
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
getLimitEvaluator(domainObject) {
const provider = this.findLimitEvaluator(domainObject);
const provider = this.#findLimitEvaluator(domainObject);
if (!provider) {
return {
evaluate: function () {}
@@ -578,7 +558,7 @@ export default class TelemetryAPI {
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
getLimits(domainObject) {
const provider = this.findLimitEvaluator(domainObject);
const provider = this.#findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) {
return {
limits: function () {

View File

@@ -23,11 +23,11 @@ import { createOpenMct, resetApplicationState } from 'utils/testing';
import TelemetryAPI from './TelemetryAPI';
import TelemetryCollection from './TelemetryCollection';
describe('Telemetry API', function () {
describe('Telemetry API', () => {
let openmct;
let telemetryAPI;
beforeEach(function () {
beforeEach(() => {
openmct = {
time: jasmine.createSpyObj('timeAPI', [
'timeSystem',
@@ -47,11 +47,11 @@ describe('Telemetry API', function () {
});
describe('telemetry providers', function () {
describe('telemetry providers', () => {
let telemetryProvider;
let domainObject;
beforeEach(function () {
beforeEach(() => {
telemetryProvider = jasmine.createSpyObj('telemetryProvider', [
'supportsSubscribe',
'subscribe',
@@ -73,19 +73,16 @@ describe('Telemetry API', function () {
};
});
it('provides consistent results without providers', function (done) {
it('provides consistent results without providers', async () => {
const unsubscribe = telemetryAPI.subscribe(domainObject);
expect(unsubscribe).toEqual(jasmine.any(Function));
telemetryAPI.request(domainObject)
.then((data) => {
expect(data).toEqual([]);
})
.finally(done);
const data = await telemetryAPI.request(domainObject);
expect(data).toEqual([]);
});
it('skips providers that do not match', function (done) {
it('skips providers that do not match', async () => {
telemetryProvider.supportsSubscribe.and.returnValue(false);
telemetryProvider.supportsRequest.and.returnValue(false);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
@@ -98,14 +95,13 @@ describe('Telemetry API', function () {
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
expect(unsubscribe).toEqual(jasmine.any(Function));
telemetryAPI.request(domainObject).then((response) => {
expect(telemetryProvider.supportsRequest)
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
expect(telemetryProvider.request).not.toHaveBeenCalled();
}).finally(done);
await telemetryAPI.request(domainObject);
expect(telemetryProvider.supportsRequest)
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
expect(telemetryProvider.request).not.toHaveBeenCalled();
});
it('sends subscribe calls to matching providers', function () {
it('sends subscribe calls to matching providers', () => {
const unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);
telemetryProvider.supportsSubscribe.and.returnValue(true);
@@ -133,7 +129,7 @@ describe('Telemetry API', function () {
expect(callback).not.toHaveBeenCalledWith('otherValue');
});
it('subscribes once per object', function () {
it('subscribes once per object', () => {
const unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);
telemetryProvider.supportsSubscribe.and.returnValue(true);
@@ -164,7 +160,7 @@ describe('Telemetry API', function () {
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
});
it('only deletes subscription cache when there are no more subscribers', function () {
it('only deletes subscription cache when there are no more subscribers', () => {
const unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);
telemetryProvider.supportsSubscribe.and.returnValue(true);
@@ -187,7 +183,7 @@ describe('Telemetry API', function () {
unsubscribeThree();
});
it('does subscribe/unsubscribe', function () {
it('does subscribe/unsubscribe', () => {
const unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);
telemetryProvider.supportsSubscribe.and.returnValue(true);
@@ -203,7 +199,7 @@ describe('Telemetry API', function () {
unsubscribe();
});
it('subscribes for different object', function () {
it('subscribes for different object', () => {
const unsubFuncs = [];
const notifiers = [];
telemetryProvider.supportsSubscribe.and.returnValue(true);
@@ -243,120 +239,120 @@ describe('Telemetry API', function () {
expect(unsubFuncs[1]).toHaveBeenCalled();
});
it('sends requests to matching providers', function (done) {
it('sends requests to matching providers', async () => {
const telemPromise = Promise.resolve([]);
telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(telemPromise);
telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject).then(() => {
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
domainObject,
jasmine.any(Object)
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
domainObject,
jasmine.any(Object)
);
}).finally(done);
await telemetryAPI.request(domainObject);
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
domainObject,
jasmine.any(Object)
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
domainObject,
jasmine.any(Object)
);
});
it('generates default request options', function (done) {
it('generates default request options', async () => {
telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject).then(() => {
const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system'
}
);
await telemetryAPI.request(domainObject);
const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
}
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system'
}
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
}
);
telemetryProvider.supportsRequest.calls.reset();
telemetryProvider.request.calls.reset();
telemetryProvider.supportsRequest.calls.reset();
telemetryProvider.request.calls.reset();
telemetryAPI.request(domainObject, {}).then(() => {
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system'
}
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system'
}
);
});
}).finally(done);
await telemetryAPI.request(domainObject, {});
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
}
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
}
);
});
it('do not overwrite existing request options', function (done) {
it('do not overwrite existing request options', async () => {
telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject, {
await telemetryAPI.request(domainObject, {
start: 20,
end: 30,
domain: 'someDomain'
}).then(() => {
const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
start: 20,
end: 30,
domain: 'someDomain',
signal
}
);
});
const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object),
{
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
}
);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
start: 20,
end: 30,
domain: 'someDomain',
signal
}
);
}).finally(done);
expect(telemetryProvider.request).toHaveBeenCalledWith(
jasmine.any(Object),
{
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
}
);
});
});
describe('metadata', function () {
describe('metadata', () => {
let mockMetadata = {};
let mockObjectType = {
definition: {}
};
beforeEach(function () {
beforeEach(() => {
telemetryAPI.addProvider({
key: 'mockMetadataProvider',
supportsMetadata() {
@@ -369,7 +365,7 @@ describe('Telemetry API', function () {
openmct.types.get.and.returnValue(mockObjectType);
});
it('respects explicit priority', function () {
it('respects explicit priority', () => {
mockMetadata.values = [
{
key: "name",
@@ -408,7 +404,7 @@ describe('Telemetry API', function () {
expect(value.hints.priority).toBe(index + 1);
});
});
it('if no explicit priority, defaults to order defined', function () {
it('if no explicit priority, defaults to order defined', () => {
mockMetadata.values = [
{
key: "name",
@@ -435,7 +431,7 @@ describe('Telemetry API', function () {
expect(value.key).toBe(mockMetadata.values[index].key);
});
});
it('respects domain priority', function () {
it('respects domain priority', () => {
mockMetadata.values = [
{
key: "name",
@@ -477,7 +473,7 @@ describe('Telemetry API', function () {
expect(values[0].key).toBe('timestamp-local');
expect(values[1].key).toBe('timestamp-utc');
});
it('respects range priority', function () {
it('respects range priority', () => {
mockMetadata.values = [
{
key: "name",
@@ -519,7 +515,7 @@ describe('Telemetry API', function () {
expect(values[0].key).toBe('cos');
expect(values[1].key).toBe('sin');
});
it('respects priority and domain ordering', function () {
it('respects priority and domain ordering', () => {
mockMetadata.values = [
{
key: "id",
@@ -588,7 +584,7 @@ describe('Telemetry API', function () {
definition: {}
};
beforeEach(function () {
beforeEach(() => {
openmct.telemetry = telemetryAPI;
telemetryAPI.addProvider({
key: 'mockMetadataProvider',
@@ -644,16 +640,14 @@ describe('Telemetery', () => {
return resetApplicationState(openmct);
});
it('should not abort request without navigation', function (done) {
it('should not abort request without navigation', async () => {
telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request({}).finally(() => {
expect(watchedSignal.aborted).toBe(false);
done();
});
await telemetryAPI.request({});
expect(watchedSignal.aborted).toBe(false);
});
it('should abort request on navigation', function (done) {
it('should abort request on navigation', (done) => {
telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request({}).finally(() => {

View File

@@ -202,8 +202,13 @@ class IndependentTimeContext extends TimeContext {
}
getUpstreamContext() {
let timeContext = this.globalTimeContext;
const objectKey = this.openmct.objects.makeKeyString(this.objectPath[this.objectPath.length - 1].identifier);
const doesObjectHaveTimeContext = this.globalTimeContext.independentContexts.get(objectKey);
if (doesObjectHaveTimeContext) {
return undefined;
}
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
//last index is the view object itself

View File

@@ -229,6 +229,25 @@ describe("The Time API", function () {
expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {
const timeContext = api.getContextForView([]);
expect(timeContext).not.toBe(null);
});
it("Without a clock, is in fixed time mode", () => {
const timeContext = api.getContextForView([]);
expect(timeContext.isRealTime()).toBe(false);
});
it("Provided a clock, is in real-time mode", () => {
const timeContext = api.getContextForView([]);
timeContext.clock('mts', {
start: 0,
end: 1
});
expect(timeContext.isRealTime()).toBe(true);
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {

View File

@@ -362,6 +362,18 @@ class TimeContext extends EventEmitter {
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in real-time mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
}
return false;
}
}
export default TimeContext;

View File

@@ -114,6 +114,8 @@ export default {
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.limitEvaluator = this.openmct
.telemetry
.limitEvaluator(this.domainObject);
@@ -134,7 +136,8 @@ export default {
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
strategy: 'latest',
timeContext: this.timeContext
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);

View File

@@ -112,11 +112,7 @@ export default {
}
},
removeFromComposition(telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
this.composition.remove(telemetryObject);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object

View File

@@ -104,10 +104,14 @@ export default {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setAxesLabels();
},
removeSeries(series) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
if (index !== undefined) {
this.$delete(this.plotSeries, index);
removeSeries(seriesKey) {
const seriesIndex = this.plotSeries.findIndex(
plotSeries => this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier)
);
const foundSeries = seriesIndex > -1;
if (foundSeries) {
this.$delete(this.plotSeries, seriesIndex);
this.setAxesLabels();
}
},

View File

@@ -68,6 +68,7 @@ export default function ClockPlugin(options) {
]
},
{
ariaLabel: "12 or 24 hour clock",
control: 'select',
options: [
{

View File

@@ -30,6 +30,12 @@
padding: $interiorMarginLg $interiorMarginLg * 2;
}
.c-condition-widget__label {
padding: $interiorMargin;
text-align: center;
white-space: normal;
}
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important;

View File

@@ -583,6 +583,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a telemetry table or plot",
label: "View type",
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);

View File

@@ -245,6 +245,9 @@ export default {
});
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
},
clearSelection() {
this.$el.click();
},
watchDisplayResize() {
const resizeObserver = new ResizeObserver(() => this.updateGrid());
@@ -478,7 +481,7 @@ export default {
});
_.pullAt(this.layoutItems, indices);
this.mutate("configuration.items", this.layoutItems);
this.$el.click();
this.clearSelection();
},
untrackItem(item) {
if (!item.identifier) {
@@ -504,15 +507,11 @@ export default {
}
if (!telemetryViewCount && !objectViewCount) {
this.removeFromComposition(keyString);
this.removeFromComposition(item);
}
},
removeFromComposition(keyString) {
let composition = this.domainObject.composition ? this.domainObject.composition : [];
composition = composition.filter(identifier => {
return this.openmct.objects.makeKeyString(identifier) !== keyString;
});
this.mutate("composition", composition);
removeFromComposition(item) {
this.composition.remove(item);
},
initializeItems() {
this.telemetryViewMap = {};
@@ -529,7 +528,10 @@ export default {
}
});
this.startTransaction();
removedItems.forEach(this.removeFromConfiguration);
return this.endTransaction();
},
isItemAlreadyTracked(child) {
let found = false;
@@ -590,7 +592,7 @@ export default {
}
});
this.mutate("configuration.items", layoutItems);
this.$el.click();
this.clearSelection();
},
orderItem(position, selectedItems) {
let delta = ORDERS[position];
@@ -773,7 +775,7 @@ export default {
this.$nextTick(() => {
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
this.$el.click(); //clear selection;
this.clearSelection();
newDomainObjectsArray.forEach(domainObject => {
this.composition.add(domainObject);
@@ -867,6 +869,20 @@ export default {
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
},
toggleGrid() {
this.showGrid = !this.showGrid;
},

View File

@@ -282,12 +282,15 @@ export default {
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
strategy: 'latest',
timeContext: this.timeContext
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);

View File

@@ -185,10 +185,24 @@ export default {
this.composition.off('add', this.addFrame);
},
methods: {
containsObject(identifier) {
if ('composition' in this.domainObject) {
return this.domainObject.composition
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
}
return false;
},
buildIdentifierMap() {
this.containers.forEach(container => {
container.frames.forEach(frame => {
let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
if (!this.containsObject(frame.domainObjectIdentifier)) {
this.removeChildObject(frame.domainObjectIdentifier);
return;
}
const keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
this.identifierMap[keystring] = true;
});
});
@@ -296,11 +310,14 @@ export default {
}
},
persist(index) {
this.startTransaction();
if (index) {
this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);
}
return this.endTransaction();
},
startContainerResizing(index) {
let beforeContainer = this.containers[index];
@@ -366,6 +383,20 @@ export default {
});
this.persist();
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}
};

View File

@@ -24,6 +24,7 @@ import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import { v4 as uuid } from 'uuid';
import _ from 'lodash';
export default class CreateAction extends PropertiesAction {
constructor(openmct, type, parentDomainObject) {
@@ -50,19 +51,12 @@ export default class CreateAction extends PropertiesAction {
return;
}
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
const existingValue = this.domainObject[key];
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
}
object = value;
_.set(this.domainObject, key, value);
});
const parentDomainObject = parentDomainObjectPath[0];
@@ -94,6 +88,12 @@ export default class CreateAction extends PropertiesAction {
dialog.dismiss();
}
/**
* @private
*/
_onCancel() {
//do Nothing
}
/**
* @private
*/
@@ -107,7 +107,7 @@ export default class CreateAction extends PropertiesAction {
}
const url = '#/browse/' + objectPath
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
.map(object => object && this.openmct.objects.makeKeyString(object.identifier))
.reverse()
.join('/');
@@ -151,6 +151,7 @@ export default class CreateAction extends PropertiesAction {
formStructure.title = 'Create a New ' + definition.name;
this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
.then(this._onSave.bind(this))
.catch(this._onCancel.bind(this));
}
}

View File

@@ -22,6 +22,8 @@
import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import _ from 'lodash';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
@@ -51,25 +53,23 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @private
*/
_onSave(changes) {
async _onSave(changes) {
if (!this.openmct.objects.isTransactionActive()) {
this.openmct.objects.startTransaction();
}
try {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
const existingValue = this.domainObject[key];
if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
}
object = value;
this.openmct.objects.mutate(this.domainObject, key, value);
this.openmct.notifications.info('Save successful');
});
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.openmct.objects.endTransaction();
} catch (error) {
this.openmct.notifications.error('Error saving objects');
console.error(error);

View File

@@ -24,6 +24,7 @@ import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import Vue from 'vue';
import { debounce } from 'lodash';
@@ -101,10 +102,15 @@ describe('EditPropertiesAction plugin', () => {
composition: []
};
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
editPropertiesAction.invoke([domainObject])
.then(() => {
done();
})
.catch(() => {
done();
});
function handleFormPropertyChange(data) {
Vue.nextTick(() => {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
expect(title.value).toEqual(domainObject.name);
@@ -118,17 +124,7 @@ describe('EditPropertiesAction plugin', () => {
const clickEvent = createMouseEvent('click');
buttons[1].dispatchEvent(clickEvent);
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
editPropertiesAction.invoke([domainObject])
.then(() => {
done();
})
.catch(() => {
done();
});
});
});
it('edit properties action saves changes', (done) => {
@@ -159,11 +155,9 @@ describe('EditPropertiesAction plugin', () => {
const deBouncedCallback = debounce(callback, 300);
unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
let changed = false;
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
editPropertiesAction.invoke([domainObject]);
function handleFormPropertyChange(data) {
Vue.nextTick(() => {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
const notes = form.querySelector('textArea');
@@ -172,27 +166,18 @@ describe('EditPropertiesAction plugin', () => {
expect(buttons[0].textContent.trim()).toEqual('OK');
expect(buttons[1].textContent.trim()).toEqual('Cancel');
if (!changed) {
expect(title.value).toEqual(domainObject.name);
expect(notes.value).toEqual(domainObject.notes);
expect(title.value).toEqual(domainObject.name);
expect(notes.value).toEqual(domainObject.notes);
// change input field value and dispatch event for it
title.focus();
title.value = newName;
title.dispatchEvent(new Event('input'));
title.blur();
// change input field value and dispatch event for it
title.focus();
title.value = newName;
title.dispatchEvent(new Event('input'));
title.blur();
changed = true;
} else {
// click ok to save form changes
const clickEvent = createMouseEvent('click');
buttons[0].dispatchEvent(clickEvent);
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
}
editPropertiesAction.invoke([domainObject]);
const clickEvent = createMouseEvent('click');
buttons[0].dispatchEvent(clickEvent);
});
});
it('edit properties action discards changes', (done) => {
@@ -217,7 +202,6 @@ describe('EditPropertiesAction plugin', () => {
})
.catch(() => {
expect(domainObject.name).toEqual(name);
done();
});

View File

@@ -598,11 +598,7 @@ export default {
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
this.composition.remove(telemetryObject);
},
refreshData(bounds, isTick) {
if (!isTick) {

View File

@@ -100,6 +100,7 @@ export default {
components: {
ToggleSwitch
},
inject: ["openmct"],
props: {
model: {
type: Object,
@@ -107,11 +108,10 @@ export default {
}
},
data() {
this.changes = {};
return {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
isDisplayUnits: this.model.value.isDisplayUnits,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
@@ -120,24 +120,15 @@ export default {
},
methods: {
onChange(event) {
const data = {
model: this.model,
value: {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
isDisplayUnits: this.isDisplayUnits,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,
max: this.max,
min: this.min,
precision: this.model.value.precision
}
let data = {
model: {}
};
if (event) {
const target = event.target;
const property = target.dataset.fieldName;
data.model.property = Array.from(this.model.property).concat([property]);
data.value = this[property];
const targetIndicator = target.parentElement.querySelector('.req-indicator');
if (targetIndicator.classList.contains('req')) {
targetIndicator.classList.add('visited');
@@ -160,13 +151,13 @@ export default {
},
toggleUseTelemetryLimits() {
this.isUseTelemetryLimits = !this.isUseTelemetryLimits;
this.onChange();
},
toggleMinMax() {
this.isDisplayMinMax = !this.isDisplayMinMax;
this.onChange();
const data = {
model: {
property: Array.from(this.model.property).concat(['isUseTelemetryLimits'])
},
value: this.isUseTelemetryLimits
};
this.$emit('onChange', data);
}
}
};

View File

@@ -45,6 +45,10 @@ export default class GoToOriginalAction {
});
}
appliesTo(objectPath) {
if (this._openmct.editor.isEditing()) {
return false;
}
let parentKeystring = objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier);
if (!parentKeystring) {

View File

@@ -31,21 +31,32 @@
:title="image.formattedTime"
>
<a
class="c-thumb__image-wrapper"
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
ref="img"
class="c-thumb__image"
:src="image.url"
fetchpriority="low"
@load="imageLoadCompleted"
>
</a>
<div
v-if="viewableArea"
class="c-thumb__viewable-area"
:style="viewableAreaStyle"
></div>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</template>
<script>
const THUMB_PADDING = 4;
const BORDER_WIDTH = 2;
export default {
props: {
image: {
@@ -63,6 +74,77 @@ export default {
realTime: {
type: Boolean,
required: true
},
viewableArea: {
type: Object,
default: function () {
return null;
}
}
},
data() {
return {
imgWidth: 0,
imgHeight: 0
};
},
computed: {
viewableAreaStyle() {
if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {
return null;
}
const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;
const imgWidth = this.imgWidth;
const imgHeight = this.imgHeight;
let translateX = imgWidth * xOffsetRatio;
let translateY = imgHeight * yOffsetRatio;
let width = imgWidth * widthRatio;
let height = imgHeight * heightRatio;
if (translateX < 0) {
width += translateX;
translateX = 0;
}
if (translateX + width > imgWidth) {
width = imgWidth - translateX;
}
if (translateX + 2 * BORDER_WIDTH > imgWidth) {
translateX = imgWidth - 2 * BORDER_WIDTH;
}
if (translateY < 0) {
height += translateY;
translateY = 0;
}
if (translateY + height > imgHeight) {
height = imgHeight - translateY;
}
if (translateY + 2 * BORDER_WIDTH > imgHeight) {
translateY = imgHeight - 2 * BORDER_WIDTH;
}
return {
'transform': `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,
'width': `${width}px`,
'height': `${height}px`
};
}
},
methods: {
imageLoadCompleted() {
if (!this.$refs.img) {
return;
}
const { width: imgWidth, height: imgHeight } = this.$refs.img;
this.imgWidth = imgWidth;
this.imgHeight = imgHeight;
}
}
};

View File

@@ -25,7 +25,7 @@
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown="arrowDownHandler"
@keydown.prevent="arrowDownHandler"
@mouseover="focusElement"
>
<div
@@ -147,7 +147,7 @@
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused)"
@click="handlePauseButton(!isPaused)"
></button>
</div>
</div>
@@ -165,6 +165,9 @@
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
:class="[{
'animate-scroll': animateThumbScroll
}]"
@scroll="handleScroll"
>
<ImageThumbnail
@@ -174,6 +177,7 @@
:active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused"
:real-time="!isFixed"
:viewable-area="focusedImageIndex === index ? viewableArea : null"
@click.native="thumbnailClicked(index)"
/>
</div>
@@ -181,7 +185,7 @@
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight('reset')"
@click="scrollToRight"
></button>
</div>
</div>
@@ -191,6 +195,7 @@
import eventHelpers from '../lib/eventHelpers';
import _ from 'lodash';
import moment from 'moment';
import Vue from 'vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
@@ -219,6 +224,8 @@ const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
export default {
name: 'ImageryView',
components: {
@@ -281,10 +288,13 @@ export default {
},
imageTranslateX: 0,
imageTranslateY: 0,
imageViewportWidth: 0,
imageViewportHeight: 0,
pan: undefined,
animateZoom: true,
imagePanned: false,
forceShowThumbnails: false
forceShowThumbnails: false,
animateThumbScroll: false
};
},
computed: {
@@ -388,6 +398,12 @@ export default {
return disabled;
},
isComposedInLayout() {
return (
this.currentView?.objectPath
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
);
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
@@ -516,6 +532,23 @@ export default {
}
return 'Alt drag to pan';
},
viewableArea() {
if (this.zoomFactor === 1) {
return null;
}
const imageWidth = this.sizedImageWidth * this.zoomFactor;
const imageHeight = this.sizedImageHeight * this.zoomFactor;
const xOffset = (imageWidth - this.imageViewportWidth) / 2;
const yOffset = (imageHeight - this.imageViewportHeight) / 2;
return {
widthRatio: this.imageViewportWidth / imageWidth,
heightRatio: this.imageViewportHeight / imageHeight,
xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,
yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight
};
}
},
watch: {
@@ -548,10 +581,10 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
this.scrollHandler();
},
deep: true
},
@@ -562,7 +595,7 @@ export default {
this.getImageNaturalDimensions();
},
bounds() {
this.scrollToFocused();
this.scrollHandler();
},
isFixed(newValue) {
const isRealTime = !newValue;
@@ -620,6 +653,8 @@ export default {
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
this.loadVisibleLayers();
// // set after render so initial scroll event is skipped
setTimeout(this.setScrollBehavior, 3 * 1000);
},
beforeDestroy() {
this.persistVisibleLayers();
@@ -826,6 +861,13 @@ export default {
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
handlePauseButton(newState) {
this.paused(newState);
if (newState) {
// need to set the focused index or the paused focus will drift
this.thumbnailClicked(this.focusedImageIndex);
}
},
paused(state) {
this.isPaused = Boolean(state);
@@ -833,38 +875,63 @@ export default {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollToRight();
this.scrollHandler();
}
},
scrollToFocused() {
async scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
return;
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
},
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
if (!domThumb) {
return;
}
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
// separate scrollTo function had to be implemented since scrollIntoView
// caused undesirable behavior in layouts
// and could not simply be scoped to the parent element
if (this.isComposedInLayout) {
await Vue.nextTick();
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
this.$refs.thumbsWrapper.scrollLeft = (
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
return;
}
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
},
async scrollToRight() {
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
if (!scrollWidth) {
return;
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
await Vue.nextTick();
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
},
async scrollHandler() {
if (this.isPaused) {
await this.scrollToFocused();
return;
}
if (this.autoScroll) {
this.scrollToRight();
}
},
setScrollBehavior(value = true) {
this.animateThumbScroll = value;
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
@@ -1063,12 +1130,12 @@ export default {
}
this.setSizedImageDimensions();
this.setImageViewport();
this.calculateViewHeight();
this.scrollToFocused();
this.scrollHandler();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
@@ -1079,6 +1146,17 @@ export default {
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
}
},
setImageViewport() {
if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {
// container is taller than wrapper
this.imageViewportWidth = this.sizedImageWidth;
this.imageViewportHeight = this.sizedImageHeight;
} else {
// container is wider than wrapper
this.imageViewportWidth = this.imageContainerWidth;
this.imageViewportHeight = this.imageContainerHeight;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
@@ -1089,9 +1167,7 @@ export default {
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.scrollHandler();
this.calculateViewHeight();
@@ -1104,7 +1180,6 @@ export default {
},
wheelZoom(e) {
e.preventDefault();
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {

View File

@@ -194,6 +194,9 @@
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.animate-scroll {
scroll-behavior: smooth;
}
}
&__auto-scroll-resume-button {
@@ -285,6 +288,13 @@
flex: 0 0 auto;
padding: 2px 3px;
}
&__viewable-area {
position: absolute;
border: 2px yellow solid;
left: 0;
top: 0;
}
}
.is-small-thumbs {

View File

@@ -481,19 +481,16 @@ describe("The Imagery View Layouts", () => {
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
await Vue.nextTick();
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
});
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
xit('should change the image zoom factor when using the zoom buttons', async () => {
await Vue.nextTick();
let imageSizeBefore;
let imageSizeAfter;
@@ -512,7 +509,6 @@ describe("The Imagery View Layouts", () => {
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();
@@ -529,6 +525,19 @@ describe("The Imagery View Layouts", () => {
done();
});
it('should display the viewable area when zoom factor is greater than 1', async () => {
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);
parent.querySelector('.t-btn-zoom-reset').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
});
it('should reset the brightness and contrast when clicking the reset button', async () => {
const viewInstance = imageryView._getInstance();
await Vue.nextTick();

View File

@@ -37,14 +37,15 @@ function myItemsInterceptor(openmct, identifierObject, name) {
return identifier.key === MY_ITEMS_KEY;
},
invoke: (identifier, object) => {
if (openmct.objects.isMissing(object)) {
if (!object || openmct.objects.isMissing(object)) {
openmct.objects.save(myItemsModel);
return myItemsModel;
}
return object;
}
},
priority: openmct.priority.HIGH
};
}

View File

@@ -889,37 +889,21 @@ export default {
this.syncUrlWithPageAndSection();
this.filterAndSortEntries();
},
activeTransaction() {
return this.openmct.objects.getActiveTransaction();
},
startTransaction() {
if (!this.openmct.editor.isEditing()) {
this.openmct.objects.startTransaction();
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
saveTransaction() {
const transaction = this.activeTransaction();
if (!transaction || this.openmct.editor.isEditing()) {
return;
async saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
}
return transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
},
cancelTransaction() {
if (!this.openmct.editor.isEditing()) {
const transaction = this.activeTransaction();
transaction.cancel()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
async cancelTransaction() {
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
}
}
}

View File

@@ -31,8 +31,8 @@ export default class OpenInNewTab {
this._openmct = openmct;
}
invoke(objectPath) {
let url = objectPathToUrl(this._openmct, objectPath);
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
window.open(url);
}
}

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