Compare commits

...

45 Commits

Author SHA1 Message Date
Andrew Henry
d8c85bf858 Example code for image thumbnail formatter 2023-01-13 16:49:26 -08:00
Even Stensberg
cdb202d8ba tooling(webpack): move webpack to its own folder (#6076) 2023-01-12 11:46:35 -08:00
dependabot[bot]
905373f294 Bump sass-loader from 13.0.2 to 13.2.0 (#5968)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 13.0.2 to 13.2.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v13.0.2...v13.2.0)

---
updated-dependencies:
- dependency-name: sass-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>
2023-01-09 16:57:54 -08:00
dependabot[bot]
60c07ab506 Bump sass from 1.56.1 to 1.57.1 (#6068)
Bumps [sass](https://github.com/sass/dart-sass) from 1.56.1 to 1.57.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.56.1...1.57.1)

---
updated-dependencies:
- dependency-name: sass
  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: Rukmini Bose (Ruki) <48999852+rukmini-bose@users.noreply.github.com>
2023-01-09 16:45:50 -08:00
dependabot[bot]
7336abc111 Bump css-loader from 6.7.1 to 6.7.3 (#6056)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.7.1 to 6.7.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.7.1...v6.7.3)

---
updated-dependencies:
- dependency-name: css-loader
  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>
2023-01-09 16:34:24 -08:00
dependabot[bot]
8fe9da89a3 Bump eslint from 8.30.0 to 8.31.0 (#6091)
Bumps [eslint](https://github.com/eslint/eslint) from 8.30.0 to 8.31.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.30.0...v8.31.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>
2023-01-06 08:56:14 -08:00
dependabot[bot]
e6bdaa957a Bump plotly.js-basic-dist from 2.14.0 to 2.17.0 (#6078)
Bumps [plotly.js-basic-dist](https://github.com/plotly/plotly.js) from 2.14.0 to 2.17.0.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.14.0...v2.17.0)

---
updated-dependencies:
- dependency-name: plotly.js-basic-dist
  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>
2023-01-03 16:27:54 -08:00
dependabot[bot]
93b5519c4b Bump karma-spec-reporter from 0.0.34 to 0.0.36 (#6058)
Bumps [karma-spec-reporter](https://github.com/tmcgee123/karma-spec-reporter) from 0.0.34 to 0.0.36.
- [Release notes](https://github.com/tmcgee123/karma-spec-reporter/releases)
- [Commits](https://github.com/tmcgee123/karma-spec-reporter/compare/v0.0.34...v0.0.36)

---
updated-dependencies:
- dependency-name: karma-spec-reporter
  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>
2023-01-03 13:32:19 -08:00
Jesse Mazzella
04ef4b369c chore: bump version to 2.1.6-SNAPSHOT (#6092) 2023-01-03 09:50:28 -08:00
Jamie V
5424a62db5 [Notebook] Handle conflicts properly (#6067)
* making a revert on failed save more clear

* only notify conflicts for non sync items in object api, spruce up notebook with better transaction tracking and observing and unobserving during transactions, structuredClone backup in monkeypatch

* WIP

* WIP debuggin

* fresh start

* dont observe in transaction objects, small changes to notebook vue to indicate saving/prevent spamming, added forceRemote flag to objects.get

* updating readability of code as well as fix issue of stuck transaction for same value entry edits

* once entry is created, click out to blur

* quick revert
;

* click outside of entry to blur and commit

* switched to enter... as suggested :)

* removing unused variable

* initializing transaction to null as we are using that now for no transaction

* fix: ensure EventSource is closed so it recovers

- Make sure to close the CouchDB EventSource as well, so that it can recover in the case where two tabs or windows are on Open MCT and one refreshes. The check on line 81 was preventing recovery since the EventSource was not closed properly.

* enhance, enhance, enhance readability

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-29 14:11:08 -08:00
Shefali Joshi
9ed9e62202 Use the current clock's timestamp to show the now line in the timestrip (#6082) 2022-12-28 22:18:47 +00:00
Jesse Mazzella
327fc826c1 fix(imagery): Unblock 'latest' strategy requests for Related Telemetry in realtime mode (#6080)
* fix: use ephemeral timeContext for thumbnail metadata requests

* fix(TEMP): use `eval-source-map`

- **!!! REVERT THIS CHANGE BEFORE MERGE !!!**

* fix: only mutate if object supports mutation

* fix: pass identifier instead of whole domainObject

* fix: add start and end bounds to request

* Revert "fix(TEMP): use `eval-source-map`"

This reverts commit 7972d8c33a.

* docs: add comments
2022-12-28 19:12:00 +00:00
Jesse Mazzella
a9e3eca35c chore: bump Playwright to v1.29 (#6004)
* chore: bump Playwright to 1.28.0

* chore: bump playwright to v1.29.0

* fix: remove `|| true` shim for codecov

* Revert "fix: remove `|| true` shim for codecov"

This reverts commit ca3766fb5a.

* docs: add instructions for upgrading Playwright
2022-12-27 14:46:19 -08:00
Andrew Henry
cbecd79f71 Do not register time system listener until we have resolve remote clock object (#6063) 2022-12-20 14:01:47 -08:00
dependabot[bot]
3deb2e3dc2 Bump moment-timezone from 0.5.38 to 0.5.40 (#6050)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.38 to 0.5.40.
- [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.38...0.5.40)

---
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>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-12-20 13:50:57 -08:00
Jesse Mazzella
d6e80447ab Mutables for the Tree 🎄 + clean up TreeItem observers and mutables properly (#6032)
* fix: refresh object after conflict error

* fix: recover from error thrown during create

- Ensure that the "Saving" modal dialog is closed

- Notify user of the error, and also print to console to catch in e2e

* fix: default selector tree item to 'mine' folder

- If create fails due to a conflict or otherwise, and the user immediately tries to "Create" again, default the selector tree's selected item to the "mine" folder (which we know exists).

* fix: don't listen to composition if Selector Tree

* refactor: remove dead code

* fix: use MutableDomainObjects in the tree

- Only use mutables and observers if NOT a SelectorTree

- Properly clean up observers and mutables when a parent item is removed from the tree

* test: verify conflicts don't break object creation

* test: verify dialog closes and object is created

* refactor(e2e): update test

- Error notification on 'My Items' folder missing was removed, so don't check for it

* test: increase timeout

* refactor(e2e): use Promise.any()

* refactor(e2e): use Promise instead of polling

* test: add 2p annotation

* test: use `waitForRequest` instead of promise

- tidy up test, add comments describing our pattern

* docs(e2e): add best practices for network tests

* refactor(e2e): avoid using Promise.any

* fix: de-reactify observer and mutable maps

* fix: destroy by path on treeItem close

* fix: don't refresh for synchronized objects

* docs: fix a typo 🔥

* fix: remove existing mutable before adding

* fix: fail fast if these aren't functions

- Remove check for typeof 'function' to not hide any potential coding errors

* fix: walk up navigationPath if item not found

* chore: fix lint errors

* fix: parse conflicted object name correctly

* fix: re-throw conflict error

* fix: Cancel edit mode on conflict
2022-12-20 13:27:51 -08:00
dependabot[bot]
1a4bd0fb55 Bump eslint from 8.29.0 to 8.30.0 (#6066)
Bumps [eslint](https://github.com/eslint/eslint) from 8.29.0 to 8.30.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.29.0...v8.30.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-20 00:42:32 +00:00
Jesse Mazzella
80f89c7609 fix: no deleted objects in locator search (#6038)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-12-19 15:21:24 -08:00
dependabot[bot]
b82649772f Bump sinon from 14.0.1 to 15.0.1 (#6057)
Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.1 to 15.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.1...v15.0.1)

---
updated-dependencies:
- dependency-name: sinon
  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>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-12-19 11:21:27 -08:00
Jon Ander Oribe
7f2ed27106 [CLA Approved] Add rows refractor (#5284)
* addRows Refractor

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-12-19 18:13:53 +00:00
Michael Rogers
57e02db6b5 Prevent scrolling the window area on new image thumb telemetry - 5867 (#5961)
* Setup a scroll handler to avoid using scrollIntoView when in a layout

* Implement a separate scroll to action when in layouts

* Simplified scroll reset event and logic

* Adjust test to capture new scroll handler

* Remove done invocation after converting to async fn

* Prevent default for arrow keys to avoid scrolling layoyut

* await scrollToFocused

* Logical or to nullish coalescing

* Removed set in favor of using isNavigatedObject api

* Apply animation style after image history has length

* Lint fixes

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-12-16 10:06:16 +01: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
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
dependabot[bot]
dad88112c4 Bump @types/lodash from 4.14.188 to 4.14.189 (#5987) 2022-11-15 13:42:48 -08:00
Jesse Mazzella
202d6d8c5d Bump version to 2.1.4-SNAPSHOT (#5974) 2022-11-14 10:48:36 -08:00
54 changed files with 1200 additions and 499 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.25.2-focal
- image: mcr.microsoft.com/playwright:v1.29.0-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.25.2 install
- run: npx playwright@1.29.0 install
- run: npm install
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb

View File

@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.25.2 install
- run: npx playwright@1.29.0 install
- run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full

176
.webpack/webpack.common.js Normal file
View File

@@ -0,0 +1,176 @@
/* global __dirname module */
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
const path = require("path");
const packageDefinition = require("../package.json");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { VueLoaderPlugin } = require("vue-loader");
let gitRevision = "error-retrieving-revision";
let gitBranch = "error-retrieving-branch";
try {
gitRevision = require("child_process")
.execSync("git rev-parse HEAD")
.toString()
.trim();
gitBranch = require("child_process")
.execSync("git rev-parse --abbrev-ref HEAD")
.toString()
.trim();
} catch (err) {
console.warn(err);
}
/** @type {import('webpack').Configuration} */
const config = {
context: path.join(__dirname, ".."),
entry: {
openmct: "./openmct.js",
generatorWorker: "./example/generator/generatorWorker.js",
couchDBChangesFeed:
"./src/plugins/persistence/couch/CouchChangesFeed.js",
inMemorySearchWorker: "./src/api/objects/InMemorySearchWorker.js",
espressoTheme: "./src/plugins/themes/espresso-theme.scss",
snowTheme: "./src/plugins/themes/snow-theme.scss"
},
output: {
globalObject: "this",
filename: "[name].js",
path: path.resolve(__dirname, "..", "dist"),
library: "openmct",
libraryTarget: "umd",
publicPath: "",
hashFunction: "xxhash64",
clean: true
},
resolve: {
alias: {
"@": path.join(__dirname, "..", "src"),
legacyRegistry: path.join(__dirname, "..", "src/legacyRegistry"),
saveAs: "file-saver/src/FileSaver.js",
csv: "comma-separated-values",
EventEmitter: "eventemitter3",
bourbon: "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"d3-scale": path.join(
__dirname,
"..",
"node_modules/d3-scale/dist/d3-scale.min.js"
),
printj: path.join(
__dirname,
"..",
"node_modules/printj/dist/printj.min.js"
),
styles: path.join(__dirname, "..", "src/styles"),
MCT: path.join(__dirname, "..", "src/MCT"),
testUtils: path.join(__dirname, "..", "src/utils/testUtils.js"),
objectUtils: path.join(
__dirname,
"..",
"src/api/objects/object-utils.js"
),
utils: path.join(__dirname, "..", "src/utils")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: "src/images/favicons",
to: "favicons"
},
{
from: "./index.html",
transform: function (content) {
return content.toString().replace(/dist\//g, "");
}
},
{
from: "src/plugins/imagery/layers",
to: "imagery"
}
]
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[name].css"
})
],
module: {
rules: [
{
test: /\.(sc|sa|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader"
},
{
loader: "resolve-url-loader"
},
{
loader: "sass-loader",
options: { sourceMap: true }
}
]
},
{
test: /\.vue$/,
use: "vue-loader"
},
{
test: /\.html$/,
type: "asset/source"
},
{
test: /\.(jpg|jpeg|png|svg)$/,
type: "asset/resource",
generator: {
filename: "images/[name][ext]"
}
},
{
test: /\.ico$/,
type: "asset/resource",
generator: {
filename: "icons/[name][ext]"
}
},
{
test: /\.(woff|woff2?|eot|ttf)$/,
type: "asset/resource",
generator: {
filename: "fonts/[name][ext]"
}
}
]
},
stats: "errors-warnings",
performance: {
// We should eventually consider chunking to decrease
// these values
maxEntrypointSize: 25000000,
maxAssetSize: 25000000
}
};
module.exports = config;

View File

@@ -6,9 +6,9 @@ OpenMCT Continuous Integration servers use this configuration to add code covera
information to pull requests.
*/
const config = require('./webpack.dev');
const config = require("./webpack.dev");
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
const CI = process.env.CI === "true";
config.devtool = CI ? false : undefined;
@@ -18,13 +18,18 @@ config.module.rules.push({
test: /\.js$/,
exclude: /(Spec\.js$)|(node_modules)/,
use: {
loader: 'babel-loader',
loader: "babel-loader",
options: {
retainLines: true,
// eslint-disable-next-line no-undef
plugins: [['babel-plugin-istanbul', {
extension: ['.js', '.vue']
}]]
plugins: [
[
"babel-plugin-istanbul",
{
extension: [".js", ".vue"]
}
]
]
}
}
});

View File

@@ -5,28 +5,28 @@ This configuration should be used for development purposes. It contains full sou
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
If OpenMCT is to be used for a production server, use webpack.prod.js instead.
*/
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const path = require('path');
const webpack = require('webpack');
const path = require("path");
const webpack = require("webpack");
module.exports = merge(common, {
mode: 'development',
mode: "development",
watchOptions: {
// Since we use require.context, webpack is watching the entire directory.
// We need to exclude any files we don't want webpack to watch.
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
ignored: [
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
'**/.*' // dotfiles and dotfolders
"**/{node_modules,dist,docs,e2e}", // All files in node_modules, dist, docs, e2e,
"**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}", // Config files
"**/*.{sh,md,png,ttf,woff,svg}", // Non source files
"**/.*" // dotfiles and dotfolders
]
},
resolve: {
alias: {
"vue": path.join(__dirname, "node_modules/vue/dist/vue.js")
vue: path.join(__dirname, "..", "node_modules/vue/dist/vue.js")
}
},
plugins: [
@@ -34,20 +34,20 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
})
],
devtool: 'eval-source-map',
devtool: "eval-source-map",
devServer: {
devMiddleware: {
writeToDisk: (filePathString) => {
const filePath = path.parse(filePathString);
const shouldWrite = !(filePath.base.includes('hot-update'));
const shouldWrite = !filePath.base.includes("hot-update");
return shouldWrite;
}
},
watchFiles: ['**/*.css'],
watchFiles: ["**/*.css"],
static: {
directory: path.join(__dirname, '/dist'),
publicPath: '/dist',
directory: path.join(__dirname, "..", "/dist"),
publicPath: "/dist",
watch: false
},
client: {

View File

@@ -4,17 +4,17 @@
This configuration should be used for production installs.
It is the default webpack configuration.
*/
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const path = require('path');
const webpack = require('webpack');
const path = require("path");
const webpack = require("webpack");
module.exports = merge(common, {
mode: 'production',
mode: "production",
resolve: {
alias: {
"vue": path.join(__dirname, "node_modules/vue/dist/vue.min.js")
vue: path.join(__dirname, "..", "node_modules/vue/dist/vue.min.js")
}
},
plugins: [
@@ -22,5 +22,5 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: 'eval-source-map'
devtool: "source-map"
});

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

View File

@@ -8,7 +8,7 @@ This document is designed to capture on the What, Why, and How's of writing and
1. [Getting Started](#getting-started)
2. [Types of Testing](#types-of-e2e-testing)
3. [Architecture](#architecture)
3. [Architecture](#test-architecture-and-ci)
## Getting Started
@@ -276,14 +276,36 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test (TODO)
### How to write a great test (WIP)
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
```js
// Fill the "Notes" section with information about the
// currently running test and its project.
const { testNotes } = page;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(testNotes);
```
#### How to write a great visual test (TODO)
#### How to write a great network test
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
- Make sure to only mock requests which are relevant to the specific behavior being tested.
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
### Best Practices
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
### Tips & Tricks (TODO)
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
@@ -378,3 +400,23 @@ A single e2e test in Open MCT is extended to run:
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
### Upgrading Playwright
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
For reference, all of the locations where the version should be updated are listed below:
#### **In `openmct`:**
- `package.json`
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
- `.circleci/config.yml`
- `.github/workflows/e2e-couchdb.yml`
- `.github/workflows/e2e-pr.yml`
#### **In `openmct-yamcs`:**
- `package.json`
- `@playwright/test` should be updated to the target version.
- `.github/workflows/yamcs-quickstart-e2e.yml`

View File

@@ -72,17 +72,19 @@ 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'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
// 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);
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
await Promise.all([

View File

@@ -27,7 +27,7 @@
const { test, expect } = require('../../pluginFixtures');
test.describe("CouchDB Status Indicator @couchdb", () => {
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
test.use({ failOnConsoleError: false });
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
test('Shows green if connected', async ({ page }) => {
@@ -71,38 +71,41 @@ test.describe("CouchDB Status Indicator @couchdb", () => {
});
});
test.describe("CouchDB initialization @couchdb", () => {
test.describe("CouchDB initialization with mocked responses @couchdb", () => {
test.use({ failOnConsoleError: false });
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
// Store any relevant PUT requests that happen on the page
const createMineFolderRequests = [];
page.on('request', req => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
createMineFolderRequests.push(req);
}
});
const mockedMissingObjectResponsefromCouchDB = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
};
// Override the first request to GET openmct/mine to return a 404
await page.route('**/openmct/mine', route => {
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
});
// Override the first request to GET openmct/mine to return a 404.
// This simulates the case of starting Open MCT with a fresh database
// and no "My Items" folder created yet.
await page.route('**/mine', route => {
route.fulfill(mockedMissingObjectResponsefromCouchDB);
}, { times: 1 });
// Go to baseURL
// Set up promise to verify that a PUT request to create "My Items"
// folder was made.
const putMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'PUT');
// Set up promise to verify that a GET request to retrieve "My Items"
// folder was made.
const getMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'GET');
// Go to baseURL.
await page.goto('./', { waitUntil: 'networkidle' });
// Verify that error banner is displayed
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
expect(bannerMessage).toEqual('Failed to retrieve object mine');
// Verify that a PUT request to create "My Items" folder was made
await expect.poll(() => createMineFolderRequests.length, {
message: 'Verify that PUT request to create "mine" folder was made',
timeout: 1000
}).toBeGreaterThanOrEqual(1);
// Wait for both requests to resolve.
await Promise.all([
putMineFolderRequest,
getMineFolderRequest
]);
});
});

View File

@@ -24,8 +24,9 @@
This test suite is dedicated to tests which verify form functionality in isolation
*/
const { test, expect } = require('../../baseFixtures');
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const genUuid = require('uuid').v4;
const path = require('path');
const TEST_FOLDER = 'test folder';
@@ -128,6 +129,108 @@ test.describe('Persistence operations @couchdb', () => {
timeout: 1000
}).toEqual(1);
});
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5982'
});
const page2 = await page.context().newPage();
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
]);
// Both pages: Click the Create button
await Promise.all([
page.click('button:has-text("Create")'),
page2.click('button:has-text("Create")')
]);
// Both pages: Click "Clock" in the Create menu
await Promise.all([
page.click(`li[role='menuitem']:text("Clock")`),
page2.click(`li[role='menuitem']:text("Clock")`)
]);
// Generate unique names for both objects
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]');
// Both pages: Fill in the 'Name' form field.
await Promise.all([
nameInput.fill(""),
nameInput.fill(`Clock:${genUuid()}`),
nameInput2.fill(""),
nameInput2.fill(`Clock:${genUuid()}`)
]);
// Both pages: Fill the "Notes" section with information about the
// currently running test and its project.
const testNotes = page.testNotes;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea');
await Promise.all([
notesInput.fill(testNotes),
notesInput2.fill(testNotes)
]);
// Page 2: Click "OK" to create the domain object and wait for navigation.
// This will update the composition of the parent folder, setting the
// conditions for a conflict error from the first page.
await Promise.all([
page2.waitForLoadState(),
page2.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page2.waitForSelector('.c-message-banner__message')
]);
// Close Page 2, we're done with it.
await page2.close();
// Page 1: Click "OK" to create the domain object and wait for navigation.
// This will trigger a conflict error upon attempting to update
// the composition of the parent folder.
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
await expect(page.locator('.c-message-banner__message', {
hasText: "Conflict detected while saving mine"
})).toBeVisible();
// Page 1: Start logging console errors from this point on
let errors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Page 1: Try to create a clock with the page that received the conflict.
const clockAfterConflict = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Page 1: Wait for save progress dialog to appear/disappear
await page.locator('.c-message-banner__message', {
hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
state: 'visible'
}).waitFor({ state: 'hidden' });
// Page 1: Navigate to 'My Items' and verify that the second clock was created
await page.goto('./#/browse/mine');
await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible();
// Verify no console errors occurred
expect(errors).toHaveLength(0);
});
});
test.describe('Form Correctness by Object Type', () => {

View File

@@ -24,22 +24,19 @@
* This test suite is dedicated to testing the Gauge component.
*/
const { test, expect } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
let 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' });
// Create the gauge
gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
});
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"]');
@@ -90,4 +87,38 @@ test.describe('Gauge', () => {
// 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

@@ -44,6 +44,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
await page.locator(entryLocator).press('Enter');
}
return notebook;

View File

@@ -26,7 +26,7 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
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

@@ -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

@@ -28,12 +28,12 @@ module.exports = (config) => {
let singleRun;
if (process.env.KARMA_DEBUG) {
webpackConfig = require('./webpack.dev.js');
browsers = ['ChromeDebugging'];
webpackConfig = require("./.webpack/webpack.dev.js");
browsers = ["ChromeDebugging"];
singleRun = false;
} else {
webpackConfig = require('./webpack.coverage.js');
browsers = ['ChromeHeadless'];
webpackConfig = require("./.webpack/webpack.coverage.js");
browsers = ["ChromeHeadless"];
singleRun = true;
}
@@ -42,28 +42,28 @@ module.exports = (config) => {
delete webpackConfig.entry;
config.set({
basePath: '',
frameworks: ['jasmine', 'webpack'],
basePath: "",
frameworks: ["jasmine", "webpack"],
files: [
'indexTest.js',
"indexTest.js",
// included means: should the files be included in the browser using <script> tag?
// We don't want them as a <script> because the shared worker source
// needs loaded remotely by the shared worker process.
{
pattern: 'dist/couchDBChangesFeed.js*',
pattern: "dist/couchDBChangesFeed.js*",
included: false
},
{
pattern: 'dist/inMemorySearchWorker.js*',
pattern: "dist/inMemorySearchWorker.js*",
included: false
},
{
pattern: 'dist/generatorWorker.js*',
pattern: "dist/generatorWorker.js*",
included: false
}
],
port: 9876,
reporters: ['spec', 'junit', 'coverage-istanbul'],
reporters: ["spec", "junit", "coverage-istanbul"],
browsers,
client: {
jasmine: {
@@ -73,8 +73,8 @@ module.exports = (config) => {
},
customLaunchers: {
ChromeDebugging: {
base: 'Chrome',
flags: ['--remote-debugging-port=9222'],
base: "Chrome",
flags: ["--remote-debugging-port=9222"],
debug: true
}
},
@@ -90,7 +90,7 @@ module.exports = (config) => {
fixWebpackSourcePaths: true,
skipFilesWithNoCoverage: true,
dir: "coverage/unit", //Sets coverage file to be consumed by codecov.io
reports: ['lcovonly']
reports: ["lcovonly"]
},
specReporter: {
maxLogLines: 5,
@@ -102,11 +102,11 @@ module.exports = (config) => {
failFast: false
},
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
"indexTest.js": ["webpack", "sourcemap"]
},
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-warnings'
stats: "errors-warnings"
},
concurrency: 1,
singleRun,

View File

@@ -1,28 +1,28 @@
{
"name": "openmct",
"version": "2.1.3-SNAPSHOT",
"version": "2.1.6-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.13.0",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.16.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.3.0",
"@types/lodash": "4.14.188",
"babel-loader": "9.0.0",
"@playwright/test": "1.29.0",
"@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",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.7.1",
"css-loader": "6.7.3",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.27.0",
"eslint": "8.31.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.7.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",
@@ -38,47 +38,47 @@
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-spec-reporter": "0.0.36",
"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.38",
"moment-timezone": "0.5.40",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.25.2",
"plotly.js-basic-dist": "2.14.0",
"playwright-core": "1.29.0",
"plotly.js-basic-dist": "2.17.0",
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sass": "1.56.1",
"sass-loader": "13.0.2",
"sinon": "14.0.1",
"sass": "1.57.1",
"sass-loader": "13.2.0",
"sinon": "15.0.1",
"style-loader": "^3.3.1",
"typescript": "4.8.4",
"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"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./webpack.dev.js",
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
"build:coverage": "webpack --config ./.webpack/webpack.coverage.js",
"build:watch": "webpack --config ./.webpack/webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",

View File

@@ -73,6 +73,10 @@ export default class Editor extends EventEmitter {
return new Promise((resolve, reject) => {
const transaction = this.openmct.objects.getActiveTransaction();
if (!transaction) {
return resolve();
}
transaction.cancel()
.then(resolve)
.catch(reject)

View File

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

View File

@@ -193,23 +193,27 @@ export default class ObjectAPI {
* @memberof module:openmct.ObjectProvider#
* @param {string} key the key for the domain object to load
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
* @param {boolean} forceRemote defaults to false. If true, will skip cached and
* dirty/in-transaction objects use and the provider.get method
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal) {
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
}
@@ -391,7 +395,6 @@ export default class ObjectAPI {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
@@ -399,7 +402,7 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
if (!isNewObject) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
@@ -410,9 +413,20 @@ export default class ObjectAPI {
}
}
return result.catch((error) => {
return result.catch(async (error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`);
} else {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
if (this.isTransactionActive()) {
this.endTransaction();
}
await this.refresh(domainObject);
}
}
throw error;

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

@@ -53,10 +53,7 @@ export default class CreateAction extends PropertiesAction {
const existingValue = this.domainObject[key];
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
value = {
...existingValue,
...value
};
value = _.merge(existingValue, value);
}
_.set(this.domainObject, key, value);
@@ -76,19 +73,21 @@ export default class CreateAction extends PropertiesAction {
title: 'Saving'
});
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
try {
await this.openmct.objects.save(this.domainObject);
const compositionCollection = await this.openmct.composition.get(parentDomainObject);
compositionCollection.add(this.domainObject);
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
this.openmct.notifications.info('Save successful');
} else {
this.openmct.notifications.error('Error saving objects');
} catch (err) {
console.error(err);
this.openmct.notifications.error(`Error saving objects: ${err}`);
} finally {
dialog.dismiss();
}
dialog.dismiss();
}
/**

View File

@@ -22,6 +22,7 @@
import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import _ from 'lodash';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
@@ -61,10 +62,7 @@ export default class EditPropertiesAction extends PropertiesAction {
Object.entries(changes).forEach(([key, value]) => {
const existingValue = this.domainObject[key];
if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) {
value = {
...existingValue,
...value
};
value = _.merge(existingValue, value);
}
this.openmct.objects.mutate(this.domainObject, key, value);

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,11 +532,28 @@ 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: {
imageHistory: {
handler(newHistory, _oldHistory) {
async handler(newHistory, oldHistory) {
const newSize = newHistory.length;
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
@@ -548,10 +581,13 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
await this.scrollHandler();
if (oldHistory?.length > 0) {
this.animateThumbScroll = true;
}
},
deep: true
},
@@ -562,7 +598,7 @@ export default {
this.getImageNaturalDimensions();
},
bounds() {
this.scrollToFocused();
this.scrollHandler();
},
isFixed(newValue) {
const isRealTime = !newValue;
@@ -752,7 +788,7 @@ export default {
}
},
persistVisibleLayers() {
if (this.domainObject.configuration) {
if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}
@@ -826,6 +862,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,7 +876,7 @@ export default {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollToRight();
this.scrollHandler();
}
},
scrollToFocused() {
@@ -843,28 +886,43 @@ export default {
}
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) {
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;
},
scrollHandler() {
if (this.isPaused) {
return this.scrollToFocused();
} else if (this.autoScroll) {
return this.scrollToRight();
}
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
@@ -1063,12 +1121,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 +1137,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 +1158,7 @@ export default {
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.scrollHandler();
this.calculateViewHeight();
@@ -1104,7 +1171,6 @@ export default {
},
wheelZoom(e) {
e.preventDefault();
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {

View File

@@ -28,6 +28,7 @@ function copyRelatedMetadata(metadata) {
return copiedMetadata;
}
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
export default class RelatedTelemetry {
constructor(openmct, domainObject, telemetryKeys) {
@@ -88,9 +89,31 @@ export default class RelatedTelemetry {
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
this[key].requestLatestFor = async (datum) => {
const options = {
// We need to create a throwaway time context and pass it along
// as a request option. We do this to "trick" the Time API
// into thinking we are in fixed time mode in order to bypass this logic:
// https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59
// Context: https://github.com/akhenry/openmct-yamcs/pull/217
const ephemeralContext = new IndependentTimeContext(
this._openmct,
this._openmct.time,
[this[key].historicalDomainObject]
);
// Stop following the global context, stop the clock,
// and set bounds.
ephemeralContext.resetContext();
const newBounds = {
start: this._openmct.time.bounds().start,
end: this._parseTime(datum),
end: this._parseTime(datum)
};
ephemeralContext.stopClock();
ephemeralContext.bounds(newBounds);
const options = {
start: newBounds.start,
end: newBounds.end,
timeContext: ephemeralContext,
strategy: 'latest'
};
let results = await this._openmct.telemetry

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

@@ -35,9 +35,11 @@ export default {
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.imageMetadataValue = { ...this.metadata.valuesForHints(['image'])[0] };
this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints(['thumbnail'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageMetadataValue);
this.imageThumbnailFormatter = this.openmct.telemetry.getValueFormatter(this.imageThumbnailMetadataValue);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize

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

@@ -50,7 +50,7 @@
<Sidebar
ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:class="sidebarClasses"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
@@ -123,6 +123,7 @@
</div>
<div
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@@ -133,6 +134,11 @@
To start a new entry, click here or drag and drop any object
</span>
</div>
<progress-bar
v-if="savingTransaction"
class="c-telemetry-table__progress-bar"
:model="{ progressPerc: undefined }"
/>
<div
v-if="selectedPage && selectedPage.isLocked"
class="c-notebook__page-locked"
@@ -183,6 +189,7 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
@@ -200,7 +207,8 @@ export default {
NotebookEntry,
Search,
SearchResults,
Sidebar
Sidebar,
ProgressBar
},
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
@@ -225,7 +233,9 @@ export default {
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {}
notebookAnnotations: {},
activeTransaction: false,
savingTransaction: false
};
},
computed: {
@@ -270,6 +280,20 @@ export default {
return this.sections[0];
},
sidebarClasses() {
let sidebarClasses = [];
if (this.showNav) {
sidebarClasses.push('is-expanded');
}
if (this.sidebarCoversEntries) {
sidebarClasses.push('c-drawer--overlays');
} else {
sidebarClasses.push('c-drawer--push');
}
return sidebarClasses;
},
showLockButton() {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
@@ -297,6 +321,8 @@ export default {
this.formatSidebar();
this.setSectionAndPageFromUrl();
this.transaction = null;
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
@@ -749,6 +775,7 @@ export default {
return section.id;
},
async newEntry(embed = null) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
@@ -891,20 +918,34 @@ export default {
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.activeTransaction = true;
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
if (this.transaction !== null) {
this.savingTransaction = true;
try {
await this.transaction.commit();
} finally {
this.endTransaction();
}
}
},
async cancelTransaction() {
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
if (this.transaction !== null) {
try {
await this.transaction.cancel();
} finally {
this.endTransaction();
}
}
},
endTransaction() {
this.openmct.objects.endTransaction();
this.transaction = null;
this.savingTransaction = false;
this.activeTransaction = false;
}
}
};

View File

@@ -74,19 +74,22 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
async function resolveNotebookEntryConflicts(localMutable, openmct) {
if (localMutable.configuration.entries) {
const FORCE_REMOTE = true;
const localEntries = structuredClone(localMutable.configuration.entries);
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
const remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE);
return applyLocalEntries(remoteObject, localEntries, openmct);
}
return true;
}
function applyLocalEntries(mutable, entries, openmct) {
function applyLocalEntries(remoteObject, entries, openmct) {
let shouldSave = false;
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
@@ -110,8 +113,13 @@ function applyLocalEntries(mutable, entries, openmct) {
});
if (shouldMutate) {
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
shouldSave = true;
openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
if (shouldSave) {
return openmct.objects.save(remoteObject);
}
}

View File

@@ -36,8 +36,8 @@ export default function () {
}
let wrappedFunction = openmct.objects.get;
openmct.objects.get = function migrate(identifier) {
return wrappedFunction.apply(openmct.objects, [identifier])
openmct.objects.get = function migrate() {
return wrappedFunction.apply(openmct.objects, [...arguments])
.then(function (object) {
if (needsMigration(object)) {
migrateObject(object)

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);
}
}

View File

@@ -28,6 +28,7 @@
connected = false;
// stop listening for events
couchEventSource.removeEventListener('message', self.onCouchMessage);
couchEventSource.close();
console.debug('🚪 Closed couch connection 🚪');
return;

View File

@@ -96,8 +96,13 @@ class CouchObjectProvider {
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
let observersForObject = this.observers[keyString];
let isInTransaction = false;
if (observersForObject) {
if (this.openmct.objects.isTransactionActive()) {
isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);
}
if (observersForObject && !isInTransaction) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
if (this.isSynchronizedObject(updatedObject)) {
@@ -219,7 +224,12 @@ class CouchObjectProvider {
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
if (body?.model && isNotebookOrAnnotationType(body.model)) {
// warn since we handle conflicts for notebooks
console.warn(error.message);
} else {
console.error(error.message);
}
throw error;
}
@@ -234,7 +244,8 @@ class CouchObjectProvider {
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);

View File

@@ -383,10 +383,8 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());

View File

@@ -83,6 +83,8 @@ export default class PlotSeries extends Model {
// Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
this.unPlottableValues = [undefined, Infinity, -Infinity];
}
/**
@@ -342,6 +344,10 @@ export default class PlotSeries extends Model {
let stats = this.get('stats');
let changed = false;
if (!stats) {
if ([Infinity, -Infinity].includes(value)) {
return;
}
stats = {
minValue: value,
minPoint: point,
@@ -350,13 +356,13 @@ export default class PlotSeries extends Model {
};
changed = true;
} else {
if (stats.maxValue < value) {
if (stats.maxValue < value && value !== Infinity) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value) {
if (stats.minValue > value && value !== -Infinity) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
@@ -419,7 +425,7 @@ export default class PlotSeries extends Model {
* @private
*/
isValueInvalid(val) {
return Number.isNaN(val) || val === undefined;
return Number.isNaN(val) || this.unPlottableValues.includes(val);
}
/**

View File

@@ -62,8 +62,8 @@ export default class RemoteClock extends DefaultClock {
}
start() {
this.openmct.time.on('timeSystem', this._timeSystemChange);
this.openmct.objects.get(this.identifier).then((domainObject) => {
this.openmct.time.on('timeSystem', this._timeSystemChange);
this.timeTelemetryObject = domainObject;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this._timeSystemChange();

View File

@@ -71,7 +71,10 @@ describe("the RemoteClock plugin", () => {
parse: (datum) => datum.key
};
beforeEach(async () => {
let objectPromise;
let requestPromise;
beforeEach(() => {
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
let clocks = openmct.time.getAllClocks();
@@ -89,7 +92,9 @@ describe("the RemoteClock plugin", () => {
spyOn(metadata, 'value').and.callThrough();
let requestPromiseResolve;
let requestPromise = new Promise((resolve) => {
let objectPromiseResolve;
requestPromise = new Promise((resolve) => {
requestPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
@@ -98,8 +103,7 @@ describe("the RemoteClock plugin", () => {
return requestPromise;
});
let objectPromiseResolve;
let objectPromise = new Promise((resolve) => {
objectPromise = new Promise((resolve) => {
objectPromiseResolve = resolve;
});
spyOn(openmct.objects, 'get').and.callFake(() => {
@@ -112,39 +116,48 @@ describe("the RemoteClock plugin", () => {
start: OFFSET_START,
end: OFFSET_END
});
await Promise.all([objectPromiseResolve, requestPromise]);
});
it('is available and sets up initial values and listeners', () => {
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
it("Does not throw error if time system is changed before remote clock initialized", () => {
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
});
it('will request/store the object based on the identifier passed in', () => {
expect(remoteClock.timeTelemetryObject).toEqual(object);
describe('once resolved', () => {
beforeEach(async () => {
await Promise.all([objectPromise, requestPromise]);
});
it('is available and sets up initial values and listeners', () => {
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
});
it('will request/store the object based on the identifier passed in', () => {
expect(remoteClock.timeTelemetryObject).toEqual(object);
});
it('will request metadata and set up formatters', () => {
expect(remoteClock.metadata).toEqual(metadata);
expect(metadata.value).toHaveBeenCalled();
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
});
it('will request the latest datum for the object it received and process the datum returned', () => {
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
expect(boundsCallback).toHaveBeenCalledWith({
start: TIME_VALUE + OFFSET_START,
end: TIME_VALUE + OFFSET_END
}, true);
});
it('will set up subscriptions correctly', () => {
expect(remoteClock._unsubscribe).toBeDefined();
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
});
});
it('will request metadata and set up formatters', () => {
expect(remoteClock.metadata).toEqual(metadata);
expect(metadata.value).toHaveBeenCalled();
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
});
it('will request the latest datum for the object it received and process the datum returned', () => {
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
expect(boundsCallback).toHaveBeenCalledWith({
start: TIME_VALUE + OFFSET_START,
end: TIME_VALUE + OFFSET_END
}, true);
});
it('will set up subscriptions correctly', () => {
expect(remoteClock._unsubscribe).toBeDefined();
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
});
});
});

View File

@@ -178,7 +178,7 @@ define([
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows, 'add');
this.tableRows.addRows(telemetryRows);
}
};
}
@@ -229,7 +229,7 @@ define([
});
});
this.tableRows.addRows(allRows, 'filter');
this.tableRows.clearRowsFromTableAndFilter(allRows);
}
updateFilters(updatedFilters) {

View File

@@ -61,30 +61,39 @@ define(
this.emit('remove', removed);
}
addRows(rows, type = 'add') {
if (this.sortOptions === undefined) {
throw 'Please specify sort options';
}
let isFilterTriggeredReset = type === 'filter';
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
// if type is filter, then it's a reset of all rows,
// need to wipe current rows
if (isFilterTriggeredReset) {
this.rows = [];
}
addRows(rows) {
let rowsToAdd = this.filterRows(rows);
this.sortAndMergeRows(rowsToAdd);
// we emit filter no matter what to trigger
// an update of visible rows
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
this.emit(type, rowsToAdd);
if (rowsToAdd.length > 0) {
this.emit('add', rowsToAdd);
}
}
clearRowsFromTableAndFilter(rows) {
let rowsToAdd = this.filterRows(rows);
// Reset of all rows, need to wipe current rows
this.rows = [];
this.sortAndMergeRows(rowsToAdd);
// We emit filter and update of visible rows
this.emit('filter', rowsToAdd);
}
filterRows(rows) {
if (Object.keys(this.columnFilters).length > 0) {
return rows.filter(this.matchesFilters, this);
}
return rows;
}
sortAndMergeRows(rows) {
const sortedRowsToAdd = this.sortCollection(rows);

View File

@@ -24,9 +24,27 @@
* Module defining url handling.
*/
export function paramsToArray(openmct) {
// parse urlParams from an object to an array.
function getUrlParams(openmct, customUrlParams = {}) {
let urlParams = openmct.router.getParams();
Object.entries(customUrlParams).forEach((urlParam) => {
const [key, value] = urlParam;
urlParams[key] = value;
});
if (urlParams['tc.mode'] === 'fixed') {
delete urlParams['tc.startDelta'];
delete urlParams['tc.endDelta'];
} else if (urlParams['tc.mode'] === 'local') {
delete urlParams['tc.startBound'];
delete urlParams['tc.endBound'];
}
return urlParams;
}
export function paramsToArray(openmct, customUrlParams = {}) {
// parse urlParams from an object to an array.
let urlParams = getUrlParams(openmct, customUrlParams);
let newTabParams = [];
for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
@@ -42,9 +60,9 @@ export function identifierToString(openmct, objectPath) {
return '#/browse/' + openmct.objects.getRelativePath(objectPath);
}
export default function objectPathToUrl(openmct, objectPath) {
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct);
let urlParams = paramsToArray(openmct, customUrlParams);
if (urlParams.length) {
url += '?' + urlParams.join('&');
}

View File

@@ -66,5 +66,14 @@ describe('the url tool', function () {
const constructedURL = objectPathToUrl(openmct, mockObjectPath);
expect(constructedURL).toContain('#/browse/mock-parent-folder/mock-folder');
});
it('can take params to set a custom url', () => {
const customParams = {
'tc.startBound': 1669911059,
'tc.endBound': 1669911082,
'tc.mode': 'fixed'
};
const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);
expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082&tc.mode=fixed');
});
});
});

View File

@@ -101,7 +101,8 @@ export default {
if (nowMarker) {
nowMarker.classList.remove('hidden');
nowMarker.style.height = this.contentHeight + 'px';
const now = this.xScale(Date.now());
const nowTimeStamp = this.openmct.time.clock().currentValue();
const now = this.xScale(nowTimeStamp);
nowMarker.style.left = now + this.offset + 'px';
}
}

View File

@@ -7,7 +7,11 @@
:checked="checked"
@change="onUserSelect($event)"
>
<span class="c-toggle-switch__slider"></span>
<span
class="c-toggle-switch__slider"
role="switch"
:aria-label="name"
></span>
</label>
<div
v-if="label && label.length"
@@ -32,6 +36,11 @@ export default {
required: false,
default: ''
},
name: {
type: String,
required: false,
default: ''
},
checked: Boolean
},
methods: {

View File

@@ -22,8 +22,10 @@
import ObjectView from './ObjectView.vue';
import StackedPlot from '../../plugins/plot/stackedPlot/StackedPlot.vue';
import Plot from '../../plugins/plot/Plot.vue';
export default {
ObjectView,
StackedPlot
StackedPlot,
Plot
};

View File

@@ -335,6 +335,7 @@ export default {
dialog.dismiss();
this.openmct.notifications.error('Error saving objects');
console.error(error);
this.openmct.editor.cancel();
});
},
saveAndContinueEditing() {

View File

@@ -76,7 +76,7 @@
<div :style="childrenHeightStyles">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="treeItem.navigationPath"
:key="`${treeItem.navigationPath}-${index}`"
:node="treeItem"
:is-selector-tree="isSelectorTree"
:selected-item="selectedItem"
@@ -174,8 +174,7 @@ export default {
itemOffset: 0,
activeSearch: false,
mainTreeTopMargin: undefined,
selectedItem: {},
observers: {}
selectedItem: {}
};
},
computed: {
@@ -277,10 +276,13 @@ export default {
this.treeResizeObserver.disconnect();
}
this.destroyObservers(this.observers);
this.destroyObservers();
this.destroyMutables();
},
methods: {
async initialize() {
this.observers = {};
this.mutables = {};
this.isLoading = true;
this.getSavedOpenItems();
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
@@ -355,8 +357,15 @@ export default {
}
this.treeItems = this.treeItems.filter((checkItem) => {
return checkItem.navigationPath === path
|| !checkItem.navigationPath.includes(path);
if (checkItem.navigationPath !== path
&& checkItem.navigationPath.includes(path)) {
this.destroyObserverByPath(checkItem.navigationPath);
this.destroyMutableByPath(checkItem.navigationPath);
return false;
}
return true;
});
this.openTreeItems.splice(pathIndex, 1);
this.removeCompositionListenerFor(path);
@@ -436,7 +445,17 @@ export default {
}, Promise.resolve()).then(() => {
if (this.isSelectorTree) {
this.treeItemSelection(this.getTreeItemByPath(navigationPath));
// If item is missing due to error in object creation,
// walk up the navigationPath until we find an item
let item = this.getTreeItemByPath(navigationPath);
while (!item) {
const startIndex = 0;
const endIndex = navigationPath.lastIndexOf('/');
navigationPath = navigationPath.substring(startIndex, endIndex);
item = this.getTreeItemByPath(navigationPath);
}
this.treeItemSelection(item);
}
});
},
@@ -537,7 +556,7 @@ export default {
composition = sortedComposition;
}
if (parentObjectPath.length) {
if (parentObjectPath.length && !this.isSelectorTree) {
let navigationPath = this.buildNavigationPath(parentObjectPath);
if (this.compositionCollections[navigationPath]) {
@@ -556,7 +575,15 @@ export default {
}
return composition.map((object) => {
this.addTreeItemObserver(object, parentObjectPath);
// Only add observers and mutables if this is NOT a selector tree
if (!this.isSelectorTree) {
if (this.openmct.objects.supportsMutation(object.identifier)) {
object = this.openmct.objects.toMutable(object);
this.addMutable(object, parentObjectPath);
}
this.addTreeItemObserver(object, parentObjectPath);
}
return this.buildTreeItem(object, parentObjectPath);
});
@@ -574,6 +601,15 @@ export default {
navigationPath
};
},
addMutable(mutableDomainObject, parentObjectPath) {
const objectPath = [mutableDomainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
// If the mutable already exists, destroy it.
this.destroyMutableByPath(navigationPath);
this.mutables[navigationPath] = () => this.openmct.objects.destroyMutable(mutableDomainObject);
},
addTreeItemObserver(domainObject, parentObjectPath) {
const objectPath = [domainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
@@ -588,30 +624,6 @@ export default {
this.sortTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
let children;
if (parentObjectPath.length) {
const parentItem = this.treeItems.find(item => item.objectPath === parentObjectPath);
const descendants = this.getChildrenInTreeFor(parentItem, true);
const parentIndex = this.treeItems.map(e => e.object).indexOf(parentObjectPath[0]);
children = await this.loadAndBuildTreeItemsFor(parentItem.object, parentItem.objectPath);
this.treeItems.splice(parentIndex + 1, descendants.length, ...children);
} else {
const root = await this.openmct.objects.get('ROOT');
children = await this.loadAndBuildTreeItemsFor(root, []);
this.treeItems = [...children];
}
for (let item of children) {
if (this.isTreeItemOpen(item)) {
this.openTreeItem(item);
}
}
},
sortTreeItems(parentObjectPath) {
const navigationPath = this.buildNavigationPath(parentObjectPath);
const parentItem = this.getTreeItemByPath(navigationPath);
@@ -662,6 +674,10 @@ export default {
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
if (domainObject.isMutable) {
this.addMutable(domainObject, parentItem.objectPath);
}
this.addTreeItemObserver(domainObject, parentItem.objectPath);
if (directDescendants.length === 0) {
@@ -692,13 +708,15 @@ export default {
},
compositionRemoveHandler(navigationPath) {
return (identifier) => {
let removeKeyString = this.openmct.objects.makeKeyString(identifier);
let parentItem = this.getTreeItemByPath(navigationPath);
let directDescendants = this.getChildrenInTreeFor(parentItem);
let removeItem = directDescendants.find(item => item.id === removeKeyString);
const removeKeyString = this.openmct.objects.makeKeyString(identifier);
const parentItem = this.getTreeItemByPath(navigationPath);
const directDescendants = this.getChildrenInTreeFor(parentItem);
const removeItem = directDescendants.find(item => item.id === removeKeyString);
// Remove the item from the tree, unobserve it, and clean up any mutables
this.removeItemFromTree(removeItem);
this.removeItemFromObservers(removeItem);
this.destroyObserverByPath(removeItem.navigationPath);
this.destroyMutableByPath(removeItem.navigationPath);
};
},
removeCompositionListenerFor(navigationPath) {
@@ -720,13 +738,6 @@ export default {
const removeIndex = this.getTreeItemIndex(item.navigationPath);
this.treeItems.splice(removeIndex, 1);
},
removeItemFromObservers(item) {
if (this.observers[item.id]) {
this.observers[item.id]();
delete this.observers[item.id];
}
},
addItemToTreeBefore(addItem, beforeItem) {
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
@@ -792,12 +803,17 @@ export default {
for (const result of results) {
if (!abortSignal.aborted) {
// Don't show deleted objects in search results
if (result.location === null) {
continue;
}
resultPromises.push(this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// if root, remove, we're not using in object path for tree
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
@@ -959,13 +975,46 @@ export default {
handleTreeResize() {
this.calculateHeights();
},
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
/**
* Destroy an observer for the given navigationPath.
*/
destroyObserverByPath(navigationPath) {
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
delete this.observers[navigationPath];
}
},
/**
* Destroy all observers.
*/
destroyObservers() {
Object.entries(this.observers).forEach(([key, unobserve]) => {
if (unobserve) {
unobserve();
}
delete observers[keyString];
delete this.observers[key];
});
},
/**
* Destroy a mutable for the given navigationPath.
*/
destroyMutableByPath(navigationPath) {
if (this.mutables[navigationPath]) {
this.mutables[navigationPath]();
delete this.mutables[navigationPath];
}
},
/**
* Destroy all mutables.
*/
destroyMutables() {
Object.entries(this.mutables).forEach(([key, destroyMutable]) => {
if (destroyMutable) {
destroyMutable();
}
delete this.mutables[key];
});
}
}

View File

@@ -1,160 +0,0 @@
/* global __dirname module */
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
const path = require('path');
const packageDefinition = require('./package.json');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader');
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
try {
gitRevision = require('child_process')
.execSync('git rev-parse HEAD')
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
} catch (err) {
console.warn(err);
}
/** @type {import('webpack').Configuration} */
const config = {
entry: {
openmct: './openmct.js',
generatorWorker: './example/generator/generatorWorker.js',
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss',
},
output: {
globalObject: 'this',
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
library: 'openmct',
libraryTarget: 'umd',
publicPath: '',
hashFunction: 'xxhash64',
clean: true
},
resolve: {
alias: {
"@": path.join(__dirname, "src"),
"legacyRegistry": path.join(__dirname, "src/legacyRegistry"),
"saveAs": "file-saver/src/FileSaver.js",
"csv": "comma-separated-values",
"EventEmitter": "eventemitter3",
"bourbon": "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"d3-scale": path.join(__dirname, "node_modules/d3-scale/dist/d3-scale.min.js"),
"printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"),
"styles": path.join(__dirname, "src/styles"),
"MCT": path.join(__dirname, "src/MCT"),
"testUtils": path.join(__dirname, "src/utils/testUtils.js"),
"objectUtils": path.join(__dirname, "src/api/objects/object-utils.js"),
"utils": path.join(__dirname, "src/utils")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'src/images/favicons',
to: 'favicons'
},
{
from: './index.html',
transform: function (content) {
return content.toString().replace(/dist\//g, '');
}
},
{
from: 'src/plugins/imagery/layers',
to: 'imagery'
}
]
}),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
})
],
module: {
rules: [
{
test: /\.(sc|sa|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
},
{
loader: 'resolve-url-loader'
},
{
loader: 'sass-loader',
options: {sourceMap: true }
}
]
},
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.html$/,
type: 'asset/source'
},
{
test: /\.(jpg|jpeg|png|svg)$/,
type: 'asset/resource',
generator: {
filename: 'images/[name][ext]'
}
},
{
test: /\.ico$/,
type: 'asset/resource',
generator: {
filename: 'icons/[name][ext]'
}
},
{
test: /\.(woff|woff2?|eot|ttf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
}
]
},
stats: 'errors-warnings',
performance: {
// We should eventually consider chunking to decrease
// these values
maxEntrypointSize: 25000000,
maxAssetSize: 25000000
}
};
module.exports = config;