Compare commits

..

59 Commits

Author SHA1 Message Date
7a2ea876be updates to testing documnetation 2022-05-19 12:17:47 -07:00
9c8ee09960 Rename a file tree object trigger an alphabetical reordering (#5187)
* Add observers to domain object and update tree func

* Remove unused argument

Co-authored-by: Michael Rogers <contact@mhrogers.com>
2022-05-19 11:21:33 -04:00
9568da9d5f Fix Example imagery 5158 (#5183)
* Created new project file

* click previous image button

* Zooms left, right, up, down

* Rebased and added my tests back

* Removed expected pause from zoom

* printing var

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-05-19 15:11:12 +00:00
2aa3b810ba Scatter plots (#4881)
* New view for plot underlays
* Update to show markers from data
* Add scatter plot x and y axes configuration
* Add color properties for scatter plots
* Add x and y axis min and max to work with underlays
* Use request API for telemetry (telemetry collections bug)
* Allow zero values
* Add pan and zoom functionality

* Glyphs and text changes for Scatter Plots
IMPORTANT: ANY MERGE CONFLICTS WITH FONT FILES SHOULD OVERRIDE USING THIS COMMIT - DO NOT MERGE CHANGES!
- Changed name to 'Scatter Plot', refined description;
- New icon glyph and SVG bg for `icon-plot-scatter`, font-files updated;
- More clarity added to underlay min/max form labels for clarity;

* Glyphs and text changes for Scatter Plots
- Add updated Icomoon file;

* Inspector refinements for Scatter Plots
- Consolidated Inspector section layout;
- Improved ColorSwatch.vue code using <template> tags to allow less brittle CSS styling;
- Improved Inspector CSS to remove overly specific selectors for `grid-row` elements;

* Enable indeendent time conductor
* Add button to remove scatter plot underlay
* Adds tests for scatter plot
* Modded look and icon of file remove button

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-05-19 14:01:33 +00:00
1cdbb34e21 Notebook entry tweaks for #4954 (#5036)
* Notebook entry tweaks for #4954
- Standardized entry layout;
- Colors, styles and padding refinements for entry elements;

Co-authored-by: Andrew Henry <andrew.k.henry@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-05-18 12:08:15 -07:00
95299336d0 New forms code needs tests #4539 (#4758)
* New forms code needs tests #4539

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Joshi <simplyrender@gmail.com>
2022-05-18 09:25:11 -07:00
b8ff5c7f33 Pan/Zoom persistence and pause handling improvements - 5068 (#5188)
* Remove pause on pan/zoom wheel or button input

* Test to ensure that pause mode is not activated during zoom

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-05-18 01:44:16 +00:00
9ede023cfa Bump copy-webpack-plugin from 10.2.0 to 11.0.0 (#5208)
Bumps [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) from 10.2.0 to 11.0.0.
- [Release notes](https://github.com/webpack-contrib/copy-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/copy-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/copy-webpack-plugin/compare/v10.2.0...v11.0.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-17 13:45:38 -07:00
308e621b5d 4863 - Object.hasOwn unsupported (#4920)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-05-17 11:23:51 -07:00
e6b5870234 Update PR Template to include cases which ignore testing instructions (#5204)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-05-17 18:07:59 +00:00
03e7d912be Warn user if telemetry not all telemetry metadata matches time system (#4996)
* warn user if telemetry not all telemetry metadata matches time system
* create spec file for telemetry collections
* Add tests (#4999)
- Test for warn if metadata does not match active TimeSystem
- Test for no warn if metadata matches active TimeSystem
* unset timeKey if no matching domain found
* Extract errors to constants file and update tests

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-05-16 11:58:36 -07:00
09da373d1c Add console error checking to our e2e suite (#5177)
Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-05-13 10:55:34 -07:00
b8d9e41c01 Bump karma-coverage from 2.1.1 to 2.2.0 (#5181)
Bumps [karma-coverage](https://github.com/karma-runner/karma-coverage) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/karma-runner/karma-coverage/releases)
- [Changelog](https://github.com/karma-runner/karma-coverage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma-coverage/compare/v2.1.1...v2.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 03:37:05 +00:00
815e7d169c Bump @percy/playwright from 1.0.2 to 1.0.3 (#5174)
Bumps [@percy/playwright](https://github.com/percy/percy-playwright) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/percy/percy-playwright/releases)
- [Commits](https://github.com/percy/percy-playwright/compare/v1.0.2...v1.0.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-09 20:26:35 -07:00
58387e0902 Prepare for release 2.0.4 (#5176)
* Bump d3-selection from 1.3.2 to 3.0.0

Bumps [d3-selection](https://github.com/d3/d3-selection) from 1.3.2 to 3.0.0.
- [Release notes](https://github.com/d3/d3-selection/releases)
- [Commits](https://github.com/d3/d3-selection/compare/v1.3.2...v3.0.0)

---
updated-dependencies:
- dependency-name: d3-selection
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* Prepare for sprint 2.0.4

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-05-09 21:38:49 +00:00
0a0826f87e Bump plotly.js-basic-dist from 2.5.0 to 2.12.0 (#5153)
* Bump plotly.js-basic-dist, plotly-gl2d from 2.5.0 to 2.12.0

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-05-09 21:28:43 +00:00
e063442d8c Bump d3-selection from 1.3.2 to 3.0.0 (#5009)
* Bump d3-selection, d3-scale and d3-axis from 1.3.2 to 3.x.x

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joshi <simplyrender@gmail.com>
2022-05-09 20:58:12 +00:00
6a5823ab5c Fix all e2e tests (#5168)
Fix all e2e tests
2022-05-09 20:25:21 +00:00
0493e5ae3c Bump moment from 2.29.1 to 2.29.3 (#5109)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.3.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/2.29.3/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-05-07 09:40:10 -07:00
24f13b6249 Time conductor real time 4914 (#5169)
* Created Time counductor input fields real-time mode

* Added click timespan button and click local clock button

* Click time offset button, input time offset in seconds

* Click the check button

* Verify time was updated on start time offset, click preceding now button

* Verify time was updated on preceding time offset button

* Added testing instructions as comment as testcase guide

* Typo in test name

* Updated Verify time was updated on time offset button to awaits

* Updated  Verify time was updated on preceding time offset button to awaits
2022-05-07 16:05:22 +00:00
221fb4d6bf [e2e] Update playwright eslint rules (#5141)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-05-06 15:42:49 +00:00
257742b45b Update the path of local.ini (#5165)
Modified the instructions to reference the homebrew location of `local.ini`

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-05-05 13:09:22 -05:00
44edec4f04 [e2e]: added test for creating and moving objects (#5128)
* added test for creating and moving objects

* Refactored and cleaned up test code

* Removed extra await in expect

* Clean up playwright default text in waits and nav

* Finished test file with second test

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-05-05 10:39:33 -07:00
ab4d0dd37f [e2e] Fix some of the plot tests (#5158)
* small general fixes

* Rename testsuite and use snapshot alias

* remove only

* Add some more determinism by waiting for Save Banner

* rename

* reduce time to fail

* add determinism

* log the process

Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-05-04 09:15:39 -07:00
c089a4760d 2.0.3 merge to master (#5157)
* Release 2.0.3

* Fix tick values for plots ticks in log mode and null check (#5119)

* [2297] When there is no display range or range, skip setting the range value when auto scale is turned off.

* If the formatted value is a number and a float, set precision to 2 decimal points.

* Fix value assignment

* Use whole numbers in log mode

* Revert whole numbers fix - need floats for values between 0 and 1.

* Handle scrolling to focused image on resize/new data (#5121)

* Scroll to focused image when view resizes - this will force scrolling to focused image when going to/from view large mode

* Scroll to the right if there is no paused focused image

* [LAD Tables] Use Telemetry Collections (#5127)

* Use telemetry collections to handle bounds checks

* added telemetry collection to alphanumeric telemetry view (#5131)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage (#5116)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage

* Remove animation from POS and CAM

* Fix transactions overwriting latest objects with stale objects on save (#5132)

* use object (map) instead of set to track dirty objects
* fix tests due to internals change

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>

* Gauge edit enabled 2.0.3 (#5133)

* Gauge plugin #4896, add edit mode

* Dynamic dial-type Gauge sizing by height and width (#5129)

* Improve sizing strategy for gauges.
* Do not install gauge by default for now

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [Telemetry Collections] Include data with start and end bounds (#5145)

* Reverts forced precision for log plots axis labels (#5147)

* Condition Widgets trigger hundreds of persistence calls (#5146)

Co-authored-by: unlikelyzero <jchill2@gmail.com>

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-05-03 11:09:12 -07:00
b77a4066f2 Use navigator platform to display separate for Linux OS - 4848 (#5115)
* Regex match the linux platform and display separate message

* Added test for different alt test based on OS in userAgent

* Simplify to use full navigator string instead of navigator.platform or userAgentData.platform

* Use userAgent string

* Test.skip depending on OS

* Remove .only after confirming test

* Adjust the skip logic

* Fix Flake

* halfbaked implementation

* Updated test to use os specific hotkeys and check for correct text

* Remove test.only

* Delete old tests

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-05-03 17:18:06 +00:00
20d7e80502 Bump github/codeql-action from 1 to 2 (#5110)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 15:31:51 +00:00
d63fec51a7 [Build] Update CircleCI Dependency to fix flakey downloads (#5123) 2022-05-02 17:34:07 -07:00
d30c4fcb53 Add Gauge plugin #4896, add edit mode (#5118)
* Add Gauge plugin #4896, add edit mode
2022-04-26 14:32:23 -07:00
fff3ce0acf [Telemetry Collection] Telemetry table excluding start and end bound values #5095 (#5096) 2022-04-23 01:49:38 +00:00
db5cb2517f Telemetry Table performance marks (#5107)
Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2022-04-22 17:49:50 -07:00
5236f1c796 Sort and merge incoming telemetry (#5042)
* use sort and merge sorted strategy for incoming data
* add shortcut for merging to beginning or end of existing rows

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-04-22 23:51:47 +00:00
1ed253cb07 Show image thumbnails in layout views - 4884 (#5099)
* Only show thumbnails if image view is > 400px high

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2022-04-22 16:14:59 -07:00
a6553ba010 Delete gauge.e2eSpec.js (#5105)
Deletes the erroneously committed e2e spec for gauges.

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-04-22 22:20:15 +00:00
cf6bc5be2d Search fix identifier (#4947)
* use identifier not key for object get calls
* re-index on composition or name changes only
* search should account for namespaces

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2022-04-22 15:15:37 -07:00
a53a3a0297 Add gauge 4896 (#4919)
* Add new Gauge component

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-04-22 14:58:08 -07:00
402cd15726 Log plots 2 / custom ticks (#4931)
* add some types to XAxisModel

* Add UI to toggle log mode.

* handle autoscale toggle for logMode

* add log plot tests

* test log ticks work after refresh

* add an initial manually-created visual snapshot test of log plot chart

* update plot unit tests for log mode

* remove scale variable for now

* make v-for keys unique per template to avoid a small performance hazard of v-for markup in the same subtree of a template having clashing keys (Vue quirk)

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-04-20 14:28:46 -07:00
a5580912e3 fix a couple flaky tests (#5061)
* fix: forgot to increase maxDiffPixels for one snapshot test, making it more chance to flake. let's see if this work

* hopefully fix PerformanceIndicator test flakes

* hopefully actually fix PerfIndicator test this time

* ok, *finally* fix PerfIndicator test... hopefully...

* simplify PerfIndicator test to check only for positive fps value
2022-04-20 10:41:09 -07:00
54d1b8991c [Build] Add support for node18 (#5091) 2022-04-19 15:05:08 -07:00
7b6acee793 Bump karma-spec-reporter from 0.0.33 to 0.0.34 (#5086)
Bumps [karma-spec-reporter](https://github.com/tmcgee123/karma-spec-reporter) from 0.0.33 to 0.0.34.
- [Release notes](https://github.com/tmcgee123/karma-spec-reporter/releases)
- [Commits](https://github.com/tmcgee123/karma-spec-reporter/compare/v0.0.33...v0.0.34)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-19 21:21:23 +00:00
04e1c60e5c Prepare for 2.0.3 release (#5087)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-19 18:53:38 +00:00
91bcd78d40 fix preview by checking before accessing key, fix delay of resize, by using leading:true option of debounce (#5054) 2022-04-19 18:34:26 +00:00
a3c0e073c8 Plots y axis and legend fixes (#5062)
* [5058] Change the unit if the yKey changes after initialization

* [5057] Show y axis label when more than one series is present with the same range value

* Fix typo for model length check

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2022-04-19 17:37:01 +00:00
21ae9f45c1 Bump resolve-url-loader from 4.0.0 to 5.0.0 (#4870)
Bumps [resolve-url-loader](https://github.com/bholloway/resolve-url-loader/tree/HEAD/packages/resolve-url-loader) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/bholloway/resolve-url-loader/releases)
- [Changelog](https://github.com/bholloway/resolve-url-loader/blob/v5/packages/resolve-url-loader/CHANGELOG.md)
- [Commits](https://github.com/bholloway/resolve-url-loader/commits/5.0.0/packages/resolve-url-loader)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2022-04-18 15:48:03 -07:00
0a40c8dd0b Bump moment-duration-format from 2.2.2 to 2.3.2 (#5010)
Bumps [moment-duration-format](https://github.com/jsmreese/moment-duration-format) from 2.2.2 to 2.3.2.
- [Release notes](https://github.com/jsmreese/moment-duration-format/releases)
- [Commits](https://github.com/jsmreese/moment-duration-format/compare/2.2.2...2.3.2)

---
updated-dependencies:
- dependency-name: moment-duration-format
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-17 20:18:13 +00:00
ef1ea8e712 Bump actions/upload-artifact from 2 to 3 (#5049)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-17 19:53:45 +00:00
5c4fad77ff Bump karma from 6.3.17 to 6.3.18 (#5071)
Bumps [karma](https://github.com/karma-runner/karma) from 6.3.17 to 6.3.18.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v6.3.17...v6.3.18)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-16 10:26:40 -07:00
dbac9e6cd2 Bump vue-eslint-parser from 8.2.0 to 8.3.0 (#5065)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 8.2.0 to 8.3.0.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v8.2.0...v8.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 17:22:30 -07:00
4b7bcf9c89 Bump eslint from 8.11.0 to 8.13.0 (#5056)
Bumps [eslint](https://github.com/eslint/eslint) from 8.11.0 to 8.13.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.11.0...v8.13.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-14 16:39:07 +00:00
2b42abd495 Update package.json (#5050)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-04-14 09:10:02 -07:00
1f2102b845 Fix duration to milliseconds converter (#5064) 2022-04-14 08:27:38 -07:00
2ccb90aa41 De-reactify tables (#5046) 2022-04-11 14:34:52 -07:00
525496fbca fix: autoscale turned off could cause errors (#5040)
* fix: autoscale turned off could cause errors

* remove commented code

* add tests for plot ticks

* make sure autoscale tests use a certain window size so they work consistently

* add commented code to use once playwright snapshot testing is fixed

* default the user selected range to the current range prior to when they turn off autoscale

* add snapshot tests for plots autoscale turned off test
2022-04-11 11:22:44 -07:00
47099786cb release 2.0.2 merge to master (#5044)
* Fix version number

* temp remove e2e-ci until percy fix (#5032)

* [Imagery] Improve View Large Action Performance (#5024)

* added the ability to pass the element you would like to enlarge to the view large action
* Example of performance marks (#5027)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Andrew Henry <andrew.k.henry@nasa.gov>

* [Notebooks] Transactions for entry creation/editing (#4917)

* adding transactions to notebook entry editing

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Andrew Henry <andrew.k.henry@nasa.gov>

* Revert "temp remove e2e-ci until percy fix (#5032)" (#5047)

This reverts commit 5b4ba7772a.

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Andrew Henry <andrew.k.henry@nasa.gov>
2022-04-08 11:35:34 -07:00
3a11291a3b Set flex direction to row reverse to right-align imagery thumbnails (#4934)
* Set flex direction to row reverse so thumbnails are right-aligned

* Flex direction to justify content

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-04-06 11:45:01 -05:00
476f1b2579 Freshness Indicators (#5002)
* Added animation-delay and animation-duration properties to inline styles

* Accept config options from plugin

* Lint fix

* Lint remove trailing space

* Lint: blank line

* Make default values consistent

* Removal of default css and cleanup

* Updated the default values for image freshness

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-04-06 16:26:00 +00:00
6153ad8e1e Add new timelist view and plugin (#4766)
* Add new timelist view and plugin
* Add inspector properties
* calculate list bounds to show/hide events
* Add timer to track 'Now' for timelist
* Styling for Timelist view

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-04-05 14:48:32 -07:00
77c0b16050 [Build] Update broken transitive percy package and override core-js (#5030)
* Update package.json

* Update package.json

* Update package.json

* override percy/cli install and move core-js

* Update package.json

* published fix

* Attempt without specific dependency

* Attempt without specific dependency

* revert

Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-04-05 13:41:33 -07:00
d19088cec6 Conditional styles for stacked plots (#4965) 2022-03-31 14:47:58 -07:00
174 changed files with 9709 additions and 1229 deletions

View File

@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.19.2-focal
- image: mcr.microsoft.com/playwright:v1.21.1-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
@ -64,7 +64,7 @@ commands:
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
orbs:
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.2.3
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
parameters:
@ -149,11 +149,15 @@ workflows:
node-version: lts/fermium
browser: ChromeHeadless
post-steps:
- upload_code_covio
- upload_code_covio
- unit-test:
name: node16-chrome
node-version: lts/gallium
browser: ChromeHeadless
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
browser: ChromeHeadless
- e2e-test:
name: e2e-ci
node-version: lts/gallium
@ -176,6 +180,10 @@ workflows:
name: node16-chrome-nightly
node-version: lts/gallium
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
browser: ChromeHeadless
- npm-audit:
node-version: lts/gallium
- e2e-test:

View File

@ -40,6 +40,7 @@ assignees: ''
- [ ] Is there a workaround available?
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug with no functional impact?
- [ ] Does this block the execution of e2e tests?
#### Additional Information
<!--- Include any screenshots, gifs, or logs which will expedite triage -->

22
.github/ISSUE_TEMPLATE/test-failure.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Test Failure
about: Flaky or Failing Tests or test suite
title: '[Test]'
labels:
- type:maintenance
- flake
assignees: ''
---
#### Testcase or Testsuite Title
#### First Failure on Master
#### First Failure on a branch or PR
#### Additional Information
[Guide to Triage Available](docs/src/process/testing/triage-test-failure.md)
<!--- Include any screenshots, gifs, or logs which will expedite triage -->

View File

@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Unit tests included and/or updated with changes?
* [ ] Command line build passes?
* [ ] Has this been smoke tested?
* [ ] Testing instructions included in associated issue?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist

View File

@ -32,12 +32,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@ -8,7 +8,7 @@ on:
jobs:
e2e-full:
if: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'pr:e2e' }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
if: ${{ github.event.label.name == 'pr:e2e' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
@ -30,21 +30,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npx playwright@1.21.1 install
- run: npm install
- run: npx playwright install
- run: npm run test:e2e:full
- name: Archive test results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Test success

View File

@ -10,25 +10,15 @@ on:
jobs:
e2e-visual:
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npx playwright@1.21.1 install
- run: npm install
- run: npx playwright install
- name: Run the e2e visual tests
run: npm run test:e2e:visual
env:

21
.github/workflows/e2e.yml vendored Normal file
View File

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

View File

@ -6,6 +6,9 @@ on:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
pull_request:
types:
- labeled
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
@ -53,7 +56,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v2
env:
@ -80,7 +82,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:

View File

@ -27,7 +27,6 @@ jobs:
with:
node-version: 16
registry-url: https://registry.npmjs.org/
check-latest: true
- run: npm install
- run: npm publish --access public --tag unstable
env:

View File

@ -12,14 +12,13 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-20.04 #MMOC Ubuntu version
- ubuntu-latest
- macos-latest
- macos-10.15
- windows-latest
node_version:
- 14
- 16
- 17
- 18
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
@ -30,16 +29,6 @@ jobs:
with:
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npm install
- run: npm test
- run: npm run lint -- --quiet

8
docs/src/process/ci.md Normal file
View File

@ -0,0 +1,8 @@
# Continuous Integration
## What runs our Continuous Integration Suite
## Why is my PR Failing?
## Test Failure

View File

@ -7,7 +7,7 @@ Testing for Open MCT includes:
* _Smoke testing_: Brief, informal testing to verify that no major issues
or regressions are present in the software, or in specific features of
the software.
* _Unit testing_: Automated verification of the performance of individual
* _Automated Testing_: Automated verification of the performance of individual
software components.
* _User testing_: Testing with a representative user base to verify
that application behaves usably and as specified.
@ -19,7 +19,9 @@ Testing for Open MCT includes:
Manual, non-rigorous testing of the software and/or specific features
of interest. Verifies that the software runs and that basic functionality
is present. The outcome of Smoke Testing should be a simplified list of Acceptance Tests which could be executed by another team member with sufficient context.
is present.
The outcome of Smoke Testing should be a simplified list of Acceptance Tests which could be executed by another team member with sufficient context. Those could also be used to feed into our e2e testing suite.
### Unit Testing
@ -27,6 +29,14 @@ Unit tests are automated tests which exercise individual software
components. Tests are subject to code review along with the actual
implementation, to ensure that tests are applicable and useful.
Unit tests should meet
[test standards](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md#test-standards)
as described in the contributing guide.
### Automated Acceptance Testing
Automated Acceptance Tests can be considered a compliment to unit tests with the key benefit that they test the application at the same layer as a user. Historically, these tests have been considered slow and unreliable, but with recent modernization and standardization of testing frameworks, those considerations can be avoided with low effort. More on our
Unit tests should meet
[test standards](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#test-standards)
as described in the contributing guide.
@ -89,11 +99,11 @@ Before changes are merged, the author of the changes must perform:
* _Smoke testing_ (both generally, and for areas which interact with
the new changes.)
* _Unit testing_ (as part of the automated build step.)
* _PR Checks_ (outlined in the [CI Document](docs/src/process/ci.md))
Changes are not merged until the author has affirmed that both
forms of testing have been performed successfully; this is documented
by the [Author Checklist](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#author-checklist).
by the [Author Checklist](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md#author-checklist).
### Per-sprint Testing

13
e2e/README.md Normal file
View File

@ -0,0 +1,13 @@
# e2e Testing
### Getting Started
### Architecture
### When to write an e2e test instead of a unit test
### RoadMap
### FAQ
### Best Practices and Recipes

27
e2e/fixtures.js Normal file
View File

@ -0,0 +1,27 @@
/* eslint-disable no-undef */
// This file extends the base functionality of the playwright test framework
const base = require('@playwright/test');
const { expect } = require('@playwright/test');
exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => {
const messages = [];
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
await use(page);
await expect.soft(messages.toString()).not.toContain('[error]');
},
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'
});
await use(vBrowser);
} else {
// Use Local Browser for testing.
await use(browser);
}
}
});

View File

@ -6,9 +6,9 @@ const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 2,
retries: 1,
testDir: 'tests',
timeout: 90 * 1000,
timeout: 60 * 1000,
webServer: {
command: 'npm run start',
port: 8080,
@ -28,12 +28,12 @@ const config = {
{
name: 'chrome',
use: {
browserName: 'chromium',
...devices['Desktop Chrome']
browserName: 'chromium'
}
},
{
name: 'MMOC',
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
viewport: {

View File

@ -29,12 +29,12 @@ const config = {
{
name: 'chrome',
use: {
browserName: 'chromium',
...devices['Desktop Chrome']
browserName: 'chromium'
}
},
{
name: 'MMOC',
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
viewport: {

View File

@ -0,0 +1,79 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify form functionality.
*/
const { test, expect } = require('@playwright/test');
const TEST_FOLDER = 'test folder';
test.describe('forms set', () => {
test('New folder form has title as required field', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.click('button:has-text("Create")');
// Click :nth-match(:text("Folder"), 2)
await page.click(':nth-match(:text("Folder"), 2)');
// Click text=Properties Title Notes >> input[type="text"]
await page.click('text=Properties Title Notes >> input[type="text"]');
// Fill text=Properties Title Notes >> input[type="text"]
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
// Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
// Click text=OK Cancel
await page.click('text=OK', { force: true });
const okButton = page.locator('text=OK');
await expect(okButton).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
// Click text=Properties Title Notes >> input[type="text"]
await page.click('text=Properties Title Notes >> input[type="text"]');
// Fill text=Properties Title Notes >> input[type="text"]
await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
// Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
});
test.fixme('Create all object types and verify correctness', async ({ page }) => {
//Create the following Domain Objects with their unique Object Types
// Sine Wave Generator (number object)
// Timer Object
// Plan View Object
// Clock Object
// Hyperlink
});
});

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify branding related components.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Branding tests', () => {
test('About Modal launches with basic branding properties', async ({ page }) => {

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Example Event Generator Operations', () => {
test('Can create example event generator with a name', async ({ page }) => {

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Sine Wave Generator', () => {
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {

View File

@ -24,19 +24,119 @@
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Move item tests', () => {
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
//Create and save Folder
//Create and save Domain Object
//Verify that the newly created domain object can be moved to Folder from Step 1.
//Verify that newly moved object appears in the correct point in Tree
//Verify that newly moved object appears correctly in Inspector panel
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
// Go to Open MCT
await page.goto('/');
// Create a new folder in the root my items folder
let folder1 = "Folder1";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// Create another folder with a new name at default location, which is currently inside Folder 1
let folder2 = "Folder2";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// Move Folder 2 from Folder 1 to My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
await page.locator(`a:has-text("${folder2}")`).click({
button: 'right'
});
await page.locator('li.icon-move').click();
await page.locator('form[name="mctForm"] >> text=My Items').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Expect that Folder 2 is in My Items, the root folder
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
});
test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
//Create and save Telemetry Object
//Create and save Domain Object
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
// Go to Open MCT
await page.goto('/');
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled = await okButton.isDisabled();
expect.soft(okButtonStateDisabled).toBeTruthy();
// Continue test regardless of assertion and create it in My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Open My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Select Folder Object and select Move from context menu
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${folder}")`).click()
]);
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
button: 'right'
});
await page.locator('li.icon-move').click();
// See if it's possible to put the folder in the Telemetry object after creation
await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled2 = await okButton2.isDisabled();
expect(okButtonStateDisabled2).toBeTruthy();
});
});

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
const path = require('path');
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {

View File

@ -24,7 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Clock Generator', () => {

View File

@ -21,45 +21,164 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Condition Set Operations', () => {
test('Create new button `condition set` creates new condition object', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
//Click the Create button
await page.click('button:has-text("Create")');
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click text=Condition Set
await page.click('text=Condition Set');
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=OK
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
//Reload Page
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
page.reload(),
page.waitForLoadState('networkidle')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
});
test.fixme('condition set object properties exist', async ({ page }) => {
//Go to object created in step one
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test.fixme('condition set object can be modified', async ({ page }) => {
//Go to object created in step one
test('condition set object can be modified on @localStorage', async ({ page }) => {
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Update the Condition Set properties
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
// Click Edit Button
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter');
// Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
});
test.fixme('condition set object can be deleted', async ({ page }) => {
//Go to object created in step one
//Verify that Condition Set object can be deleted
//Verify the Condition Set object does not exist in Tree
//Verify the Condition Set object does not exist with direct navigation to object's URL
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Right Click to Open Actions Menu
await page.locator('a:has-text("Unnamed Condition Set")').click({
button: 'right'
});
// Click Remove Action
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature?
//Domain Object is still available by direct URL after delete
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
});
});

View File

@ -24,13 +24,15 @@
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
but only assume that example imagery is present.
*/
/* globals process */
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()))
page.on('console', msg => console.log(msg.text()));
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@ -40,12 +42,18 @@ test.describe('Example Imagery', () => {
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
});
@ -77,9 +85,11 @@ test.describe('Example Imagery', () => {
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover();
@ -91,40 +101,49 @@ test.describe('Example Imagery', () => {
// center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
//Get Diagnostic info about process environment
console.log('process.platform is ' + process.platform);
const getUA = await page.evaluate(() => navigator.userAgent);
console.log('navigator.userAgent ' + getUA);
// Pan Imagery Hints
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
// pan right
await page.keyboard.down('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left
await page.keyboard.down('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// pan up
await page.mouse.move(imageCenterX, imageCenterY);
await page.keyboard.down('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down
await page.keyboard.down('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
@ -156,20 +175,26 @@ test.describe('Example Imagery', () => {
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = await page.locator(backgroundImageSelector);
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox();
@ -180,38 +205,160 @@ test.describe('Example Imagery', () => {
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
});
//test('Can use Mouse Wheel to zoom in and out of previous image');
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await bgImageLocator.hover();
// open the time conductor drop down
await page.locator('.c-conductor__controls button.c-mode-button').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in');
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
});
test.describe('Example Imagery in Display layout', () => {
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
// The following test case will cover these scenarios
// ('Can use Mouse Wheel to zoom in and out of previous image');
// ('Can use alt+drag to move around image once zoomed in');
// ('Clicking on the left arrow should pause the imagery and go to previous image');
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Example Imagery in Display layout', async ({ page }) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Clear and set Image load delay (milliseconds)
await page.click('input[type="number"]', {clickCount: 3})
await page.type('input[type="number"]', "20")
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// Click previous image button
const previousImageButton = await page.locator('.c-nav--prev');
await previousImageButton.click();
// Verify previous image
const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible();
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover();
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
// Pan Imagery Hints
console.log('process.platform is '+process.platform);
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
// Click next image button
const nextImageButton = await page.locator('.c-nav--next');
await nextImageButton.click();
// Click fixed timespan button
await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
// Zoom in on next image
await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await bgImageLocator.hover();
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Click previous image button
await previousImageButton.click();
// Verify previous image
await expect(selectedImage).toBeVisible();
// Wait 20ms to verify no new image has come in
await page.waitForTimeout(21);
//Get background-image url from background-image css prop
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1)
// sleep 21ms
await page.waitForTimeout(21);
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl2 ' + backgroundImageUrl2)
// Expect backgroundImageUrl2 to be greater then backgroundImageUrl1
expect(backgroundImageUrl2 >= backgroundImageUrl1);
});
test.describe('Example Imagery in Flexible layout', () => {
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
});
test.describe('Example Imagery in Tabs view', () => {
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
});

View File

@ -0,0 +1,198 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Testsuite for plot autoscale.
*/
const { test: _test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
// create a new `test` API that will not append platform details to snapshot
// file names, only for the tests in this file, so that the same snapshots will
// be used for all platforms.
const test = _test.extend({
_autoSnapshotSuffix: [
async ({}, use, testInfo) => {
testInfo.snapshotSuffix = '';
await use();
},
{ auto: true }
]
});
test.use({
viewport: {
width: 1280,
height: 720
}
});
test.describe('ExportAsJSON', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
//This is necessary due to the size of the test suite.
await test.setTimeout(120 * 1000);
await page.goto('/', { waitUntil: 'networkidle' });
await setTimeRange(page);
await createSinewaveOverlayPlot(page);
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
await turnOffAutoscale(page);
const canvas = page.locator('canvas').nth(1);
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
await Promise.all([
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
]);
await page.keyboard.down('Alt');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 200,
y: 200
},
targetPosition: {
x: 400,
y: 400
}
});
await page.keyboard.up('Alt');
// Ensure the drag worked.
await Promise.all([
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
]);
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} start
* @param {string} end
*/
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill(start);
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill(end);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function createSinewaveOverlayPlot(page) {
// click create button
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function turnOffAutoscale(page) {
// enter edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
// save
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testYTicks(page, values) {
const yTicks = page.locator('.gl-plot-y-tick-label');
await page.locator('canvas >> nth=1').hover();
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
for (let i = 0, l = values.length; i < l; i += 1) {
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
}
await Promise.all(promises);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,305 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
//This is necessary due to the size of the test suite.
await test.setTimeout(120 * 1000);
await makeOverlayPlot(page);
await testRegularTicks(page);
await enableEditMode(page);
await enableLogMode(page);
await testLogTicks(page);
await disableLogMode(page);
await testRegularTicks(page);
await enableLogMode(page);
await testLogTicks(page);
await saveOverlayPlot(page);
await testLogTicks(page);
//await testLogPlotPixels(page);
// refresh page and wait for charts and ticks to load
await page.waitForTimeout(1 * 1000);
await page.reload({ waitUntil: 'networkidle'});
await page.waitForSelector('.gl-plot-chart-area');
await page.waitForSelector('.gl-plot-y-tick-label');
// test log ticks hold up after refresh
await testLogTicks(page);
//await testLogPlotPixels(page);
});
test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
await makeOverlayPlot(page);
await enableEditMode(page);
await enableLogMode(page);
await saveOverlayPlot(page);
// TODO ...export, delete the overlay, then import it...
//await testLogTicks(page);
// TODO, the plot is slightly at different position that in the other test, so this fails.
// ...We can fix it by copying all steps from the first test...
// await testLogPlotPixels(page);
});
});
/**
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
* @param {import('@playwright/test').Page} page
*/
async function makeOverlayPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save the overlay plot
await saveOverlayPlot(page);
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// click on overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testRegularTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(7);
await expect(yTicks.nth(0)).toHaveText('-2');
await expect(yTicks.nth(1)).toHaveText('0');
await expect(yTicks.nth(2)).toHaveText('2');
await expect(yTicks.nth(3)).toHaveText('4');
await expect(yTicks.nth(4)).toHaveText('6');
await expect(yTicks.nth(5)).toHaveText('8');
await expect(yTicks.nth(6)).toHaveText('10');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testLogTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(28);
await expect(yTicks.nth(0)).toHaveText('-2.98');
await expect(yTicks.nth(1)).toHaveText('-2.50');
await expect(yTicks.nth(2)).toHaveText('-2.00');
await expect(yTicks.nth(3)).toHaveText('-1.51');
await expect(yTicks.nth(4)).toHaveText('-1.20');
await expect(yTicks.nth(5)).toHaveText('-1.00');
await expect(yTicks.nth(6)).toHaveText('-0.80');
await expect(yTicks.nth(7)).toHaveText('-0.58');
await expect(yTicks.nth(8)).toHaveText('-0.40');
await expect(yTicks.nth(9)).toHaveText('-0.20');
await expect(yTicks.nth(10)).toHaveText('-0.00');
await expect(yTicks.nth(11)).toHaveText('0.20');
await expect(yTicks.nth(12)).toHaveText('0.40');
await expect(yTicks.nth(13)).toHaveText('0.58');
await expect(yTicks.nth(14)).toHaveText('0.80');
await expect(yTicks.nth(15)).toHaveText('1.00');
await expect(yTicks.nth(16)).toHaveText('1.20');
await expect(yTicks.nth(17)).toHaveText('1.51');
await expect(yTicks.nth(18)).toHaveText('2.00');
await expect(yTicks.nth(19)).toHaveText('2.50');
await expect(yTicks.nth(20)).toHaveText('2.98');
await expect(yTicks.nth(21)).toHaveText('3.50');
await expect(yTicks.nth(22)).toHaveText('4.00');
await expect(yTicks.nth(23)).toHaveText('4.50');
await expect(yTicks.nth(24)).toHaveText('5.31');
await expect(yTicks.nth(25)).toHaveText('7.00');
await expect(yTicks.nth(26)).toHaveText('8.00');
await expect(yTicks.nth(27)).toHaveText('9.00');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableEditMode(page) {
// turn on edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableLogMode(page) {
// turn on log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function disableLogMode(page) {
// turn off log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function saveOverlayPlot(page) {
// save overlay plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testLogPlotPixels(page) {
const pixelsMatch = await page.evaluate(async () => {
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
await new Promise((r) => setTimeout(r, 5 * 1000));
// These are some pixels that should be blue points in the log plot.
// If the plot changes shape to an unexpected shape, this will
// likely fail, which is what we want.
//
// I found these pixels by pausing playwright in debug mode at this
// point, and using similar code as below to output the pixel data, then
// I logged those pixels here.
const expectedBluePixels = [
// TODO these pixel sets only work with the first test, but not the second test.
// [60, 35],
// [121, 125],
// [156, 377],
// [264, 73],
// [372, 186],
// [576, 73],
// [659, 439],
// [675, 423]
[60, 35],
[120, 125],
[156, 375],
[264, 73],
[372, 185],
[575, 72],
[659, 437],
[675, 421]
];
// The first canvas in the DOM is the one that has the plot point
// icons (canvas 2d), which is the one we are testing. The second
// one in the DOM is the WebGL canvas with the line. (Why aren't
// they both WebGL?)
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
for (const pixel of expectedBluePixels) {
// XXX Possible optimization: call getImageData only once with
// area including all pixels to be tested.
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
// If any pixel is empty, it means we didn't hit a plot point.
return false;
}
}
return true;
});
expect(pixelsMatch).toBe(true);
}

View File

@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('@playwright/test');
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Time counductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
@ -67,3 +68,45 @@ test.describe('Time counductor operations', () => {
expect(endDateValidityStatus).not.toBeTruthy();
});
});
// Testing instructions:
// Try to change the realtime offsets when in realtime (local clock) mode.
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Set realtime "local clock" mode offsets
const timeInputs = page.locator('input.c-input--datetime');
// Click fixed timespan button
await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
// Click time offset button
await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
// Input start time offset
await page.fill('.pr-time-controls__secs', '23');
// Click the check button
await page.locator('.icon-check').click();
// Verify time was updated on time offset button
await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
// Click time offset set preceding now button
await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
// Input preceding time offset
await page.fill('.pr-time-controls__secs', '31');
// Click the check buttons
await page.locator('.icon-check').click();
// Verify time was updated on preceding time offset button
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
});
});

View File

@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting
Make no assumptions about the order that elements appear in the DOM.
*/
const { test, expect } = require('@playwright/test');
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {

View File

@ -77,7 +77,7 @@
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
@ -195,6 +195,7 @@
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.start();
</script>
</html>

View File

@ -1,13 +1,13 @@
{
"name": "openmct",
"version": "2.0.2-SNAPSHOT",
"version": "2.0.4-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.0.0-beta.76",
"@percy/playwright": "1.0.1",
"@playwright/test": "1.19.2",
"@percy/cli": "1.0.4",
"@percy/playwright": "1.0.3",
"@playwright/test": "1.21.1",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
@ -17,16 +17,15 @@
"babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "10.2.0",
"core-js": "3.21.1",
"copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "4.0.0",
"d3-axis": "1.0.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"eslint": "8.11.0",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.8.0",
"eslint-plugin-playwright": "0.9.0",
"eslint-plugin-vue": "8.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
@ -38,38 +37,38 @@
"imports-loader": "0.8.0",
"jasmine-core": "4.0.1",
"jsdoc": "3.5.5",
"karma": "6.3.15",
"karma": "6.3.18",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.1.1",
"karma-coverage": "2.2.0",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "2.1.2",
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
"moment": "2.29.1",
"moment-duration-format": "2.2.2",
"moment": "2.29.3",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.34",
"node-bourbon": "4.2.3",
"painterro": "1.2.56",
"plotly.js-basic-dist": "2.5.0",
"plotly.js-gl2d-dist": "2.5.0",
"plotly.js-basic-dist": "2.12.0",
"plotly.js-gl2d-dist": "2.12.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "4.0.0",
"resolve-url-loader": "5.0.0",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sinon": "13.0.1",
"style-loader": "^1.0.1",
"uuid": "3.3.3",
"vue": "2.6.14",
"vue-eslint-parser": "8.2.0",
"vue-eslint-parser": "8.3.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
@ -93,8 +92,9 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock exampleImagery",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
@ -112,6 +112,9 @@
"engines": {
"node": ">=14.19.1"
},
"overrides": {
"core-js": "3.21.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",

View File

@ -241,9 +241,9 @@ define([
this.branding = BrandingAPI.default;
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.ScatterPlot());
this.install(this.plugins.BarChart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());

View File

@ -1,5 +1,6 @@
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
import CheckBoxField from './components/controls/CheckBoxField.vue';
import Datetime from './components/controls/Datetime.vue';
import FileInput from './components/controls/FileInput.vue';
import Locator from './components/controls/Locator.vue';
@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue';
import SelectField from './components/controls/SelectField.vue';
import TextAreaField from './components/controls/TextAreaField.vue';
import TextField from './components/controls/TextField.vue';
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
import Vue from 'vue';
export const DEFAULT_CONTROLS_MAP = {
'autocomplete': AutoCompleteField,
'checkbox': CheckBoxField,
'composite': ClockDisplayFormatField,
'datetime': Datetime,
'file-input': FileInput,
@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = {
'numberfield': NumberField,
'select': SelectField,
'textarea': TextAreaField,
'textfield': TextField
'textfield': TextField,
'toggleSwitch': ToggleSwitchField
};
export default class FormControl {
@ -94,4 +98,3 @@ export default class FormControl {
};
}
}

View File

@ -23,10 +23,13 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
import EventEmitter from 'EventEmitter';
import Vue from 'vue';
export default class FormsAPI {
export default class FormsAPI extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.formController = new FormController(openmct);
}
@ -107,6 +110,8 @@ export default class FormsAPI {
let onDismiss;
let onSave;
const self = this;
const promise = new Promise((resolve, reject) => {
onSave = onFormSave(resolve);
onDismiss = onFormDismiss(reject);
@ -115,7 +120,7 @@ export default class FormsAPI {
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: this.openmct
openmct: self.openmct
},
data() {
return {
@ -132,7 +137,7 @@ export default class FormsAPI {
if (element) {
element.append(formElement);
} else {
overlay = this.openmct.overlays.overlay({
overlay = self.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
onDestroy: () => vm.$destroy()
@ -140,6 +145,7 @@ export default class FormsAPI {
}
function onFormPropertyChange(data) {
self.emit('onFormPropertyChange', data);
if (onChange) {
onChange(data);
}

View File

@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Forms API', () => {
let openmct;
let element;
beforeEach((done) => {
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless(element);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('openmct supports form API', () => {
expect(openmct.forms).not.toBe(null);
});
describe('check default form controls exists', () => {
it('autocomplete', () => {
const control = openmct.forms.getFormControl('autocomplete');
expect(control).not.toBe(null);
});
it('clock', () => {
const control = openmct.forms.getFormControl('composite');
expect(control).not.toBe(null);
});
it('datetime', () => {
const control = openmct.forms.getFormControl('datetime');
expect(control).not.toBe(null);
});
it('file-input', () => {
const control = openmct.forms.getFormControl('file-input');
expect(control).not.toBe(null);
});
it('locator', () => {
const control = openmct.forms.getFormControl('locator');
expect(control).not.toBe(null);
});
it('numberfield', () => {
const control = openmct.forms.getFormControl('numberfield');
expect(control).not.toBe(null);
});
it('select', () => {
const control = openmct.forms.getFormControl('select');
expect(control).not.toBe(null);
});
it('textarea', () => {
const control = openmct.forms.getFormControl('textarea');
expect(control).not.toBe(null);
});
it('textfield', () => {
const control = openmct.forms.getFormControl('textfield');
expect(control).not.toBe(null);
});
});
it('supports user defined form controls', () => {
const newFormControl = {
show: () => {
console.log('show new control');
},
destroy: () => {
console.log('destroy');
}
};
openmct.forms.addNewFormControl('newFormControl', newFormControl);
const control = openmct.forms.getFormControl('newFormControl');
expect(control).not.toBe(null);
expect(control.show).not.toBe(null);
expect(control.destroy).not.toBe(null);
});
describe('show form on UI', () => {
let formStructure;
beforeEach(() => {
formStructure = {
title: 'Test Show Form',
sections: [
{
rows: [
{
key: 'name',
control: 'textfield',
name: 'Title',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: 'Test Name'
}
]
}
]
};
});
it('when container element is provided', (done) => {
openmct.forms.showForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');
expect(titleElement.textContent).toBe(formStructure.title);
element.querySelector('.js-cancel-button').click();
});
it('when container element is not provided', (done) => {
openmct.forms.showForm(formStructure).catch(() => {
done();
});
const titleElement = document.querySelector('.c-overlay__dialog-title');
const title = titleElement.textContent;
expect(title).toBe(formStructure.title);
document.querySelector('.js-cancel-button').click();
});
});
});

View File

@ -21,9 +21,9 @@
*****************************************************************************/
<template>
<div class="c-form">
<div class="c-form js-form">
<div class="c-overlay__top-bar c-form__top-bar">
<div class="c-overlay__dialog-title">{{ model.title }}</div>
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
</div>
<form
@ -70,7 +70,7 @@
</button>
<button
tabindex="0"
class="c-button"
class="c-button js-cancel-button"
@click="onDismiss"
>
{{ cancelLabel }}

View File

@ -79,10 +79,12 @@ export default {
rowClass() {
let cssClass = this.cssClass;
if (this.row.required) {
cssClass = `${cssClass} req`;
if (!this.row.required) {
return;
}
cssClass = `${cssClass} req`;
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
cssClass = `${cssClass} valid`;

View File

@ -0,0 +1,55 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
type="checkbox"
:checked="isChecked"
@input="toggleCheckBox"
>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
export default {
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isChecked: this.model.value
};
}
};
</script>

View File

@ -40,6 +40,12 @@
>
{{ name }}
</button>
<button
v-if="removable"
class="c-button icon-trash"
title="Remove file"
@click="removeFile"
></button>
</span>
</span>
</template>
@ -63,6 +69,9 @@ export default {
const fileInfo = this.fileInfo || this.model.value;
return fileInfo && fileInfo.name || this.model.text;
},
removable() {
return (this.fileInfo || this.model.value) && this.model.removable;
}
},
mounted() {
@ -97,6 +106,15 @@ export default {
},
selectFile() {
this.$refs.fileInput.click();
},
removeFile() {
this.model.value = undefined;
this.fileInfo = undefined;
const data = {
model: this.model,
value: undefined
};
this.$emit('onChange', data);
}
}
};

View File

@ -58,7 +58,6 @@ export default {
},
methods: {
updateText() {
console.log('updateText', this.field);
const data = {
model: this.model,
value: this.field

View File

@ -0,0 +1,62 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
id="switchId"
:checked="isChecked"
@change="toggleCheckBox"
/>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
import uuid from 'uuid';
export default {
components: {
ToggleSwitch
},
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
switchId: `toggleSwitch-${uuid}`,
isChecked: this.model.value
};
}
};
</script>

View File

@ -0,0 +1,19 @@
export default {
data() {
return {
isChecked: false
};
},
methods: {
toggleCheckBox(event) {
this.isChecked = !this.isChecked;
const data = {
model: this.model,
value: this.isChecked
};
this.$emit('onChange', data);
}
}
};

View File

@ -26,29 +26,31 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
describe ('The Menu API', () => {
let openmct;
let element;
let appHolder;
let menuAPI;
let actionsArray;
let x;
let y;
let result;
let onDestroy;
let menuElement;
const x = 8;
const y = 16;
const menuOptions = {
onDestroy: () => {
console.log('default onDestroy');
}
};
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder = document.createElement('div');
appHolder.style.display = 'block';
appHolder.style.width = '1920px';
appHolder.style.height = '1080px';
openmct = createOpenMct();
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
openmct.on('start', done);
openmct.startHeadless(appHolder);
openmct.startHeadless();
menuAPI = new MenuAPI(openmct);
actionsArray = [
@ -56,7 +58,7 @@ describe ('The Menu API', () => {
key: 'test-css-class-1',
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action',
description: 'This is a test action 1',
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
@ -65,149 +67,165 @@ describe ('The Menu API', () => {
key: 'test-css-class-2',
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action',
description: 'This is a test action 2',
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}
];
x = 8;
y = 16;
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("showMenu method", () => {
it("creates an instance of Menu when invoked", () => {
menuAPI.showMenu(x, y, actionsArray);
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
describe('showMenu method', () => {
beforeAll(() => {
spyOn(menuOptions, 'onDestroy').and.callThrough();
});
describe("creates a menu component", () => {
let menuComponent;
let vueComponent;
it('creates an instance of Menu when invoked', (done) => {
menuOptions.onDestroy = done;
beforeEach(() => {
onDestroy = jasmine.createSpy('onDestroy');
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const menuOptions = {
onDestroy
};
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
document.body.click();
});
describe('creates a menu component', () => {
it('with all the actions passed in', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
vueComponent = menuAPI.menuComponent.component;
menuComponent = document.querySelector(".c-menu");
menuElement = document.querySelector('.c-menu');
expect(menuElement).toBeDefined();
spyOn(vueComponent, '$destroy');
});
it("renders a menu component in the expected x and y coordinates", () => {
let boundingClientRect = menuComponent.getBoundingClientRect();
let left = boundingClientRect.left;
let top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
});
it("with all the actions passed in", () => {
expect(menuComponent).toBeDefined();
let listItems = menuComponent.children[0].children;
const listItems = menuElement.children[0].children;
expect(listItems.length).toEqual(actionsArray.length);
document.body.click();
});
it("with click-able menu items, that will invoke the correct callBacks", () => {
let listItem1 = menuComponent.children[0].children[0];
it('with click-able menu items, that will invoke the correct callBack', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
listItem1.click();
expect(result).toEqual("Test Action 1 Invoked");
expect(result).toEqual('Test Action 1 Invoked');
});
it("dismisses the menu when action is clicked on", () => {
let listItem1 = menuComponent.children[0].children[0];
it('dismisses the menu when action is clicked on', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
listItem1.click();
let menu = document.querySelector('.c-menu');
menuElement = document.querySelector('.c-menu');
expect(menu).toBeNull();
expect(menuElement).toBeNull();
});
it("invokes the destroy method when menu is dismissed", () => {
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
it("invokes the onDestroy callback if passed in", () => {
document.body.click();
it('invokes the onDestroy callback if passed in', (done) => {
let count = 0;
menuOptions.onDestroy = () => {
count++;
expect(count).toEqual(1);
done();
};
expect(onDestroy).toHaveBeenCalled();
menuAPI.showMenu(x, y, actionsArray, menuOptions);
document.body.click();
});
});
});
describe("superMenu method", () => {
it("creates a superMenu", () => {
menuAPI.showSuperMenu(x, y, actionsArray);
describe('superMenu method', () => {
it('creates a superMenu', (done) => {
menuOptions.onDestroy = done;
const superMenu = document.querySelector('.c-super-menu__menu');
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
expect(superMenu).not.toBeNull();
expect(menuElement).not.toBeNull();
document.body.click();
});
it("Mouse over a superMenu shows correct description", (done) => {
menuAPI.showSuperMenu(x, y, actionsArray);
it('Mouse over a superMenu shows correct description', (done) => {
menuOptions.onDestroy = done;
const superMenu = document.querySelector('.c-super-menu__menu');
const superMenuItem = superMenu.querySelector('li');
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
const superMenuItem = menuElement.querySelector('li');
const mouseOverEvent = createMouseEvent('mouseover');
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
setTimeout(() => {
menuAPI.menuComponent.component.$nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
expect(superMenu).not.toBeNull();
done();
}, 300);
document.body.click();
});
});
});
describe("Menu Placements", () => {
it("default menu position BOTTOM_RIGHT", () => {
menuAPI.showMenu(x, y, actionsArray);
const menu = document.querySelector('.c-menu');
const boundingClientRect = menu.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
});
it("menu position BOTTOM_RIGHT", () => {
const menuOptions = {
placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
};
describe('Menu Placements', () => {
it('default menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const menu = document.querySelector('.c-menu');
const boundingClientRect = menu.getBoundingClientRect();
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
it('menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
});
});

View File

@ -36,13 +36,14 @@ class InMemorySearchProvider {
*/
this.MAX_CONCURRENT_REQUESTS = 100;
/**
* If max results is not specified in query, use this as default.
*/
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@ -58,7 +59,6 @@ class InMemorySearchProvider {
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
this.openmct.on('start', this.startIndexing);
this.openmct.on('destroy', () => {
@ -68,6 +68,9 @@ class InMemorySearchProvider {
this.worker.port.onmessageerror = null;
this.worker.port.close();
}
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
}
@ -137,7 +140,7 @@ class InMemorySearchProvider {
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
const domainObject = await this.openmct.objects.get(identifier);
return domainObject;
}));
@ -213,29 +216,52 @@ class InMemorySearchProvider {
}
}
onMutationOfIndexedObject(domainObject) {
onNameMutation(domainObject, name) {
const provider = this;
provider.index(domainObject.identifier, domainObject);
domainObject.name = name;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) {
const provider = this;
const indexedComposition = domainObject.composition;
const identifiersToIndex = composition
.filter(identifier => !indexedComposition
.some(indexedIdentifier => this.openmct.objects
.areIdsEqual([identifier, indexedIdentifier])));
identifiersToIndex.forEach(identifier => {
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
});
}
/**
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
* Pass a domainObject to the worker to be indexed.
* If the object has composition, schedule those ids for later indexing.
* Watch for object changes and re-index object and children if so
*
* @private
* @param id a model id
* @param model a model
* @param domainObject a domainObject
*/
async index(id, domainObject) {
async index(domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(id);
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.indexedIds[keyString]) {
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
this.indexedIds[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.onNameMutation.bind(this, domainObject)
);
this.indexedCompositions[keyString] = this.openmct.objects.observe(
domainObject,
'composition',
this.onCompositionMutation.bind(this, domainObject)
);
}
this.indexedIds[keyString] = true;
if ((id.key !== 'ROOT')) {
if ((keyString !== 'ROOT')) {
if (this.worker) {
this.worker.port.postMessage({
request: 'index',
@ -247,15 +273,12 @@ class InMemorySearchProvider {
}
}
const composition = this.openmct.composition.registry.find(foundComposition => {
return foundComposition.appliesTo(domainObject);
});
const composition = this.openmct.composition.get(domainObject);
if (composition) {
const childIdentifiers = await composition.load(domainObject);
childIdentifiers.forEach(function (childIdentifier) {
provider.scheduleForIndexing(childIdentifier);
});
if (composition !== undefined) {
const children = await composition.load();
children.forEach(child => provider.scheduleForIndexing(child.identifier));
}
}
@ -271,12 +294,12 @@ class InMemorySearchProvider {
const provider = this;
this.pendingRequests += 1;
const identifier = await this.openmct.objects.parseKeyString(keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
const domainObject = await this.openmct.objects.get(keyString);
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
await provider.index(identifier, domainObject);
await provider.index(domainObject);
}
} catch (error) {
console.warn('Failed to index domain object ' + keyString, error);
@ -305,9 +328,9 @@ class InMemorySearchProvider {
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
this.localIndexedItems[keyString] = {
type: model.type,
@ -347,6 +370,16 @@ class InMemorySearchProvider {
};
this.onWorkerMessage(eventToReturn);
}
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
unobserve();
}
delete observers[keyString];
});
}
}
export default InMemorySearchProvider;

View File

@ -105,13 +105,18 @@ describe("The Object API Search Function", () => {
beforeEach((done) => {
openmct = createOpenMct();
const defaultObjectProvider = openmct.objects.getProvider({
key: '',
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'some-namespace'
namespace: 'foo'
};
mockDomainObject1 = {
type: 'clock',
@ -120,7 +125,7 @@ describe("The Object API Search Function", () => {
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'some-namespace'
namespace: 'foo'
};
mockDomainObject2 = {
type: 'clock',
@ -129,16 +134,16 @@ describe("The Object API Search Function", () => {
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'some-namespace'
namespace: 'foo'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
done();
});
openmct.startHeadless();
@ -175,9 +180,9 @@ describe("The Object API Search Function", () => {
beforeEach(async () => {
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
});
it("calls local search", () => {
openmct.objects.search('foo');

View File

@ -22,12 +22,14 @@
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = new Set();
this.dirtyObjects = {};
this.objectAPI = objectAPI;
}
add(object) {
this.dirtyObjects.add(object);
const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
}
cancel() {
@ -37,7 +39,8 @@ export default class Transaction {
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
@ -48,7 +51,9 @@ export default class Transaction {
return new Promise((resolve, reject) => {
action(object)
.then((success) => {
this.dirtyObjects.delete(object);
const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
resolve(success);
})
.catch(reject);
@ -57,7 +62,8 @@ export default class Transaction {
getDirtyObject(identifier) {
let dirtyObject;
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
@ -67,14 +73,11 @@ export default class Transaction {
return dirtyObject;
}
start() {
this.dirtyObjects = new Set();
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});

View File

@ -34,24 +34,24 @@ describe("Transaction Class", () => {
});
it('has no dirty objects', () => {
expect(transaction.dirtyObjects.size).toEqual(0);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
});
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
transaction.cancel()
.then(success => {
expect(transaction.dirtyObjects.size).toEqual(0);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}).finally(done);
});
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction.commit()
.then(success => {
expect(transaction.dirtyObjects.size).toEqual(0);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done);
});
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(transaction.dirtyObjects.size).toEqual(0);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined);

View File

@ -512,7 +512,7 @@ define([
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
return supportsRequest && hasRequestProvider;
});

View File

@ -22,11 +22,7 @@
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
const ERRORS = {
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
LOADED: 'Telemetry Collection has already been loaded.'
};
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
/** Class representing a Telemetry Collection. */
@ -61,7 +57,7 @@ export class TelemetryCollection extends EventEmitter {
*/
load() {
if (this.loaded) {
this._error(ERRORS.LOADED);
this._error(LOADED_ERROR);
}
this._setTimeSystem(this.openmct.time.timeSystem());
@ -172,6 +168,7 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) {
return;
}
@ -266,6 +263,10 @@ export class TelemetryCollection extends EventEmitter {
this.lastBounds = bounds;
if (isTick) {
if (this.timeKey === undefined) {
return;
}
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
@ -305,7 +306,6 @@ export class TelemetryCollection extends EventEmitter {
if (added.length > 0) {
this.emit('add', added);
}
} else {
// user bounds change, reset
this._reset();
@ -325,12 +325,16 @@ export class TelemetryCollection extends EventEmitter {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain === undefined) {
this._error(ERRORS.TIMESYSTEM_KEY);
if (domain !== undefined) {
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source;
} else {
this.timeKey = undefined;
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source; // this defaults to key if no source is set
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
@ -352,6 +356,7 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually
*/
_reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = [];
this.futureBuffer = [];
@ -400,4 +405,8 @@ export class TelemetryCollection extends EventEmitter {
_error(message) {
throw new Error(message);
}
_warn(message) {
console.warn(message);
}
}

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { TIMESYSTEM_KEY_WARNING } from './constants';
describe('Telemetry Collection', () => {
let openmct;
let mockMetadataProvider;
let mockMetadata = {};
let domainObject;
beforeEach(done => {
openmct = createOpenMct();
openmct.on('start', done);
domainObject = {
identifier: {
key: 'a',
namespace: 'b'
},
type: 'sample-type'
};
mockMetadataProvider = {
key: 'mockMetadataProvider',
supportsMetadata() {
return true;
},
getMetadata() {
return mockMetadata;
}
};
openmct.telemetry.addProvider(mockMetadataProvider);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState();
});
it('Warns if telemetry metadata does not match the active timesystem', () => {
mockMetadata.values = [
{
key: 'foo',
name: 'Bar',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
});
it('Does not warn if telemetry metadata matches the active timesystem', () => {
mockMetadata.values = [
{
key: 'utc',
name: 'Timestamp',
format: 'utc',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';

View File

@ -40,11 +40,13 @@ describe("The User API", () => {
});
afterEach(() => {
const activeOverlays = openmct.overlays.activeOverlays;
activeOverlays.forEach(overlay => overlay.dismiss());
return resetApplicationState(openmct);
});
describe('with regard to user providers', () => {
it('allows you to specify a user provider', () => {
openmct.user.on('providerAdded', (provider) => {
expect(provider).toBeInstanceOf(ExampleUserProvider);

View File

@ -1,4 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -114,14 +113,12 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
.limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@ -135,72 +132,41 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
if (this.hasUnits) {
this.setUnit();
}
},
destroyed() {
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.telemetryCollection.destroy();
},
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
this.updatingView = false;
});
}
},
setLatestValues(datum) {
this.latestDatum = datum;
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {
this.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.resetValues();
this.timestampKey = timeSystem.key;
},
updateViewContext() {
@ -241,4 +207,3 @@ export default {
}
};
</script>

View File

@ -46,6 +46,7 @@ describe("The LAD Table", () => {
let openmct;
let ladPlugin;
let historicalProvider;
let parent;
let child;
let telemetryCount = 3;
@ -81,6 +82,13 @@ describe("The LAD Table", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
openmct.time.bounds({
start: bounds.start,
end: bounds.end
@ -147,7 +155,7 @@ describe("The LAD Table", () => {
// add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
beforeEach(async () => {
beforeEach(async (done) => {
let telemetryRequestResolve;
let telemetryObjectResolve;
let anotherTelemetryObjectResolve;
@ -166,11 +174,12 @@ describe("The LAD Table", () => {
callBack();
});
openmct.telemetry.request.and.callFake(() => {
historicalProvider.request = () => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
};
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry);
@ -195,6 +204,8 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
done();
});
it("should show one row per object in the composition", () => {

View File

@ -40,6 +40,14 @@ export default {
BarGraph
},
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
@ -247,7 +255,7 @@ export default {
}
});
const trace = {
let trace = {
key,
name: telemetryObject.name,
x: xValues,
@ -255,13 +263,18 @@ export default {
text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar',
type: this.options.type ? this.options.type : 'bar',
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: 'skip'
};
if (this.options.type) {
trace.mode = 'markers';
trace.hoverinfo = 'x+y';
}
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { SCATTER_PLOT_KEY } from './scatterPlotConstants';
export default function ScatterPlotCompositionPolicy(openmct) {
function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']).map((value) => {
return value.source;
});
const uniqueRangeValues = new Set(rangeValues);
return uniqueRangeValues && uniqueRangeValues.size > 1;
}
function hasScatterPlotTelemetry(domainObject) {
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasRange(metadata);
}
return {
allow: function (parent, child) {
if (parent.type === SCATTER_PLOT_KEY) {
if ((child.type === 'conditionSet') || (!hasScatterPlotTelemetry(child))) {
return false;
}
}
return true;
}
};
}

View File

@ -0,0 +1,146 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<span class="form-control">
<span
class="field control"
:class="model.cssClass"
>
<div
class="c-form--sub-grid"
>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Minimum X axis value</label>
<input
ref="domainMin"
v-model.number="domainMin"
data-field-name="domainMin"
type="number"
@input="onChange('domainMin')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Maximum X axis value</label>
<input
ref="domainMax"
v-model.number="domainMax"
data-field-name="domainMax"
type="number"
@input="onChange('domainMax')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Minimum Y axis value</label>
<input
ref="rangeMin"
v-model.number="rangeMin"
data-field-name="rangeMin"
type="number"
@input="onChange('rangeMin')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Maximum Y axis value</label>
<input
ref="rangeMax"
v-model.number="rangeMax"
data-field-name="rangeMax"
type="number"
@input="onChange('rangeMax')"
>
</div>
</div>
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
rangeMax: this.model.value.rangeMax,
rangeMin: this.model.value.rangeMin,
domainMax: this.model.value.domainMax,
domainMin: this.model.value.domainMin
};
},
computed: {
isRequired() {
return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(value => value !== undefined && value !== '');
}
},
methods: {
onChange(property) {
if (this[property] === '') {
this[property] = undefined;
}
const data = {
model: this.model,
value: {
rangeMax: this.rangeMax,
rangeMin: this.rangeMin,
domainMax: this.domainMax,
domainMin: this.domainMin
}
};
if (property) {
this.model.validate(data);
}
this.$emit('onChange', data);
}
}
};
</script>

View File

@ -0,0 +1,346 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<ScatterPlotWithUnderlay
class="c-plot c-scatter-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue';
import _ from 'lodash';
export default {
components: {
ScatterPlotWithUnderlay
},
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.valuesByTimestamp = {};
this.subscriptions = [];
return {
trace: []
};
},
computed: {
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
return {
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
};
}
},
mounted() {
this.setTimeContext();
this.loadComposition();
this.reloadTelemetry = this.reloadTelemetry.bind(this);
this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
},
beforeDestroy() {
this.stopFollowingTimeContext();
if (!this.composition) {
return;
}
this.removeAllSubscriptions();
this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeTelemetryObject);
if (this.unobserve) {
this.unobserve();
}
if (this.unobserveUnderlayRanges) {
this.unobserveUnderlayRanges();
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetry);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetry);
}
},
addToComposition(telemetryObject) {
if (Object.values(this.telemetryObjects).length > 0) {
this.confirmRemoval(telemetryObject);
} else {
this.addTelemetryObject(telemetryObject);
}
},
removeFromComposition(telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
this.getDataForTelemetry(key);
},
confirmRemoval(telemetryObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
this.removeFromComposition(oldTelemetryObject);
this.removeTelemetryObject(oldTelemetryObject.identifier);
this.valuesByTimestamp = {};
this.addTelemetryObject(telemetryObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(telemetryObject);
dialog.dismiss();
}
}
]
});
},
getTelemetryProcessor(keyString) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
const telemetryObject = this.telemetryObjects[keyString];
if (!telemetryObject) {
return;
}
telemetry.forEach(datum => {
this.addDataToGraph(telemetryObject, datum);
});
this.updateTrace(telemetryObject);
};
},
getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
return metadata.valuesForHints(['range']);
},
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
reloadTelemetry() {
this.valuesByTimestamp = {};
Object.keys(this.telemetryObjects).forEach(key => {
this.getDataForTelemetry(key);
});
},
getDataForTelemetry(key) {
const telemetryObject = this.telemetryObjects[key];
if (!telemetryObject) {
return;
}
const telemetryProcessor = this.getTelemetryProcessor(key);
const options = this.getOptions();
this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor);
this.subscribeToObject(telemetryObject);
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
if (this.telemetryObjects[key]) {
delete this.telemetryObjects[key];
}
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
this.removeSubscription(key);
},
addDataToGraph(telemetryObject, data) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) {
this.openmct.notifications.alert(data.message);
}
if (!this.domainObject.configuration.axes.xKey || !this.domainObject.configuration.axes.yKey) {
return;
}
const timestamp = this.getTimestampForDatum(data, key, telemetryObject);
let valueForTimestamp = this.valuesByTimestamp[timestamp] || {};
//populate x values
let metadataKey = this.domainObject.configuration.axes.xKey;
if (data[metadataKey] !== undefined) {
valueForTimestamp.x = this.format(key, metadataKey, data);
}
metadataKey = this.domainObject.configuration.axes.yKey;
if (data[metadataKey] !== undefined) {
valueForTimestamp.y = this.format(key, metadataKey, data);
}
this.valuesByTimestamp[timestamp] = valueForTimestamp;
},
updateTrace(telemetryObject) {
const xAndyValues = Object.values(this.valuesByTimestamp);
const xValues = xAndyValues.map(value => value.x);
const yValues = xAndyValues.map(value => value.y);
const axisMetadata = this.getAxisMetadata(telemetryObject);
const xAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.xKey);
let yAxisMetadata = {};
if (this.domainObject.configuration.axes.yKey) {
yAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.yKey);
}
let trace = {
key: this.openmct.objects.makeKeyString(this.domainObject.identifier),
name: this.domainObject.name,
x: xValues,
y: yValues,
text: yValues.map(String),
xAxisMetadata: xAxisMetadata,
yAxisMetadata: yAxisMetadata,
type: 'scatter',
mode: 'markers',
marker: {
color: this.domainObject.configuration.styles.color
},
hoverinfo: 'x+y'
};
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.domainMin !== undefined && this.domainObject.configuration.ranges.domainMax !== undefined) {
trace.xaxis = {
min: this.domainObject.configuration.ranges.domainMin,
max: this.domainObject.configuration.ranges.domainMax
};
}
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.rangeMin !== undefined && this.domainObject.configuration.ranges.rangeMax !== undefined) {
trace.yaxis = {
min: this.domainObject.configuration.ranges.rangeMin,
max: this.domainObject.configuration.ranges.rangeMax
};
}
this.trace = [trace];
},
getTimestampForDatum(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
return this.parse(key, metadataValue.source, datum);
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
getOptions() {
const { start, end } = this.timeContext.bounds();
return {
end,
start
};
},
subscribeToObject(telemetryObject) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.removeSubscription(key);
const options = this.getOptions();
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.addDataToGraph(telemetryObject, data)
, options);
this.subscriptions.push({
key,
unsubscribe
});
},
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
},
removeAllSubscriptions() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
},
removeSubscription(key) {
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
if (found > -1) {
this.subscriptions[found].unsubscribe();
this.subscriptions.splice(found, 1);
}
}
}
};
</script>

View File

@ -0,0 +1,79 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ScatterPlotView from './ScatterPlotView.vue';
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
import Vue from 'vue';
export default function ScatterPlotViewProvider(openmct) {
function isCompactView(objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY);
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {
key: SCATTER_PLOT_VIEW,
name: 'Scatter Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
ScatterPlotView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<scatter-plot-view :options="options"></scatter-plot-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -0,0 +1,393 @@
<template>
<div
ref="plotWrapper"
class="has-local-controls"
:class="{ 's-unsynced' : isZoomed }"
>
<div
v-if="isZoomed"
class="l-state-indicators"
>
<span
class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
></span>
</div>
<div
ref="plot"
class="c-scatter-chart"
></div>
<div
ref="localControl"
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
>
<button
v-if="data.length"
class="c-button icon-reset"
:disabled="!isZoomed"
title="Reset pan/zoom"
@click="reset()"
>
</button>
</div>
</div>
</template>
<script>
import Plotly from 'plotly-basic';
const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8,
RIGHT: 94
};
import { getValidatedData } from "@/plugins/plan/util";
const PATH_COLORS = ['blue', 'red', 'green'];
const MARKER_COLOR = 'white';
export default {
inject: ['openmct', 'domainObject'],
props: {
data: {
type: Array,
default() {
return [];
}
},
plotAxisTitle: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isZoomed: false,
yAxisRange: {
min: '',
max: ''
},
xAxisRange: {
min: '',
max: ''
}
};
},
watch: {
data: {
immediate: false,
handler: 'updateData'
}
},
mounted() {
this.getUnderlayPlotData();
Plotly.newPlot(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
this.$refs.plot.on('plotly_relayout', this.zoom);
},
beforeDestroy() {
if (this.$refs.plot && this.$refs.plot.off) {
this.$refs.plot.off('plotly_relayout', this.zoom);
}
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
if (this.unlistenUnderlay) {
this.unlistenUnderlay();
}
if (this.unlistenUnderlayRanges) {
this.unlistenUnderlayRanges();
}
if (this.unobserveColorChanges) {
this.unobserveColorChanges();
}
},
methods: {
getUnderlayPlotData() {
if (this.domainObject.selectFile) {
this.shapesData = getValidatedData(this.domainObject);
} else {
this.shapesData = [];
}
},
observeForUnderlayPlotChanges() {
this.getUnderlayPlotData();
this.updateData();
},
getAxisMinMax() {
if (!this.data.length) {
return;
}
// For now, use x and y axes min, max values only if an underlay is available
if (this.shapesData.length && this.data[0].xaxis) {
this.xAxisRange = this.data[0].xaxis;
}
if (this.shapesData.length && this.data[0].yaxis) {
this.yAxisRange = this.data[0].yaxis;
}
},
getLayout() {
this.getAxisMinMax();
const yAxesMeta = this.getYAxisMeta();
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
const shapes = this.shapesData.map((shapeData, index) => {
if (!shapeData.x || !shapeData.y
|| !shapeData.x.length || !shapeData.y.length
|| shapeData.x.length !== shapeData.y.length) {
return "";
}
let path = `M ${shapeData.x[0]},${shapeData.y[0]}`;
shapeData.x.forEach((point, shapeIndex) => {
if (shapeIndex > 0) {
path = `${path} L${point},${shapeData.y[shapeIndex]}`;
}
});
return {
path,
type: 'path',
line: {
color: PATH_COLORS[index]
},
opacity: 0.5
};
});
return {
autosize: true,
showlegend: false,
textposition: 'auto',
font: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: '#666'
},
xaxis: {
domain: xAxisDomain,
range: [this.xAxisRange.min, this.xAxisRange.max],
title: this.plotAxisTitle.xAxisTitle,
automargin: true
},
yaxis: primaryYaxis,
margin: {
l: 5,
r: 5,
t: 5,
b: 0
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
shapes,
layer: 'below'
};
},
getYAxisMeta() {
const yAxisMeta = {};
this.data.forEach(datum => {
const yAxisMetadata = datum.yAxisMetadata;
const range = '1';
const side = 'left';
const name = yAxisMetadata.name;
const unit = yAxisMetadata.units;
yAxisMeta[range] = {
range,
side,
name,
unit
};
});
return yAxisMeta;
},
getXAxisDomain(yAxisMeta) {
let leftPaddingPerc = 0;
let rightPaddingPerc = 100;
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
if (yAxisMeta && rightSide.length > 1) {
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
}
if (yAxisMeta && leftSide.length > 1) {
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
}
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
},
getYaxisLayout(yAxisMeta) {
if (!yAxisMeta) {
return {};
}
const { name, range, side = 'left', unit } = yAxisMeta;
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
const yaxis = {
automargin: true,
title
};
let yRange = this.yAxisRange;
if (range === '1') {
yaxis.range = [yRange.min, yRange.max];
return yaxis;
}
yaxis.range = [yRange.min, yRange.max];
yaxis.anchor = side.toLowerCase() === 'left'
? 'free'
: 'x';
yaxis.showline = side.toLowerCase() === 'left';
yaxis.side = side.toLowerCase();
yaxis.overlaying = 'y';
yaxis.position = 0.01;
return yaxis;
},
registerListeners() {
this.unobserveColorChanges = this.openmct.objects.observe(this.domainObject, 'configuration.styles.color', this.updateColors);
this.unlistenUnderlay = this.openmct.objects.observe(this.domainObject, 'selectFile', this.observeForUnderlayPlotChanges);
this.unlistenUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.updateData);
this.resizeTimer = false;
if (window.ResizeObserver) {
this.plotResizeObserver = new ResizeObserver(() => {
// debounce and trigger window resize so that plotly can resize the plot
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
updateColors() {
const colors = [];
const indices = [];
this.data.forEach((item, index) => {
const colorExists = this.domainObject.configuration.styles.color;
indices.push(index);
if (colorExists) {
colors.push(this.domainObject.configuration.styles.color);
} else {
colors.push(item.marker.color);
}
});
const plotUpdate = {
'marker.color': colors
};
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
},
reset() {
this.isZoomed = false;
this.updatePlot();
this.$emit('subscribe');
},
updateData() {
this.updatePlot();
},
updateLocalControlPosition() {
const localControl = this.$refs.localControl;
localControl.style.display = 'none';
const plot = this.$refs.plot;
const bgLayer = this.$el.querySelector('.bglayer');
const plotBoundingRect = plot.getBoundingClientRect();
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
localControl.style.top = `${top}px`;
localControl.style.left = `${left}px`;
localControl.style.display = 'block';
},
updatePlot() {
if (!this.$refs || !this.$refs.plot || this.isZoomed) {
return;
}
Plotly.react(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout());
},
zoom(eventData) {
const autorange = eventData['xaxis.autorange'];
const { autosize } = eventData;
if (autosize || autorange) {
return;
}
this.isZoomed = true;
this.$emit('unsubscribe');
},
getShapes() {
let markerData = {
x: [],
y: []
};
const shapes = this.shapesData.map((shapeData, index) => {
if (!shapeData.x || !shapeData.y
|| !shapeData.x.length || !shapeData.y.length
|| shapeData.x.length !== shapeData.y.length) {
return "";
}
let text = [];
shapeData.x.forEach((point) => {
text.push(`${parseFloat(point).toPrecision(2)}`);
});
markerData.x = markerData.x.concat(shapeData.x);
markerData.y = markerData.y.concat(shapeData.y);
return {
x: shapeData.x,
y: shapeData.y,
mode: 'text',
text,
textfont: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: PATH_COLORS[index]
},
opacity: 0.5
};
});
shapes.push({
x: markerData.x,
y: markerData.y,
mode: "markers",
marker: {
size: 6,
color: MARKER_COLOR
}
});
return shapes;
}
}
};
</script>

View File

@ -0,0 +1,64 @@
<!--
Open MCT, Copyright (c) 2014-2022, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div>
<div v-if="canEdit">
<plot-options-edit />
</div>
<div v-else>
<plot-options-browse />
</div>
</div>
</template>
<script>
import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
import PlotOptionsEdit from "./PlotOptionsEdit.vue";
export default {
components: {
PlotOptionsBrowse,
PlotOptionsEdit
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@ -0,0 +1,153 @@
<!--
Open MCT, Copyright (c) 2014-2022, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="js-plot-options-browse grid-properties">
<ul class="l-inspector-part">
<h2 title="Object view settings">Settings</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection"
>X Axis</div>
<div class="grid-cell value">{{ xKeyLabel }}</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
title="Y axis selection"
>Y Axis</div>
<div class="grid-cell value">{{ yKeyLabel }}</div>
</li>
<ColorSwatch
:current-color="currentColor"
edit-title="Manually set the color for this plot"
view-title="The marker color for this plot"
short-label="Color"
/>
</ul>
</div>
</template>
<script>
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
import Color from "../../../../ui/color/Color";
import ColorPalette from "../../../../ui/color/ColorPalette";
export default {
components: { ColorSwatch },
inject: ['openmct', 'domainObject'],
data() {
return {
xKeyLabel: '',
yKeyLabel: '',
currentColor: undefined
};
},
mounted() {
this.plotSeries = [];
this.colorPalette = new ColorPalette();
this.initColor();
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
this.stopListening();
},
methods: {
initColor() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.styles = {
color
};
}
this.currentColor = this.domainObject.configuration.styles.color;
const colorObject = Color.fromHexString(this.currentColor);
this.colorPalette.remove(colorObject);
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setAxesLabels);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setAxesLabels();
},
removeSeries(series) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
if (index !== undefined) {
this.$delete(this.plotSeries, index);
this.setAxesLabels();
}
},
setAxesLabels() {
let xKeyOptions = [];
let yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
const series = this.plotSeries[0];
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
metadataValues.forEach((metadataValue) => {
xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
});
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name;
}
}
if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) {
yKeyOptionIndex = yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1) {
this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name;
}
}
}
}
};
</script>

View File

@ -0,0 +1,262 @@
<!--
Open MCT, Copyright (c) 2014-2022, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="js-plot-options-edit grid-properties">
<ul class="l-inspector-part">
<h2 title="Object view settings">Settings</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection."
>X Axis</div>
<div class="grid-cell value">
<select
v-model="xKey"
@change="updateForm('xKey')"
>
<option
v-for="option in xKeyOptions"
:key="`xKey-${option.value}`"
:value="option.value"
:selected="option.value == xKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
title="Y axis selection."
>Y Axis</div>
<div class="grid-cell value">
<select
v-model="yKey"
@change="updateForm('yKey')"
>
<option
v-for="option in yKeyOptions"
:key="`yKey-${option.value}`"
:value="option.value"
:selected="option.value == yKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<ColorSwatch
:current-color="currentColor"
title="Manually set the line and marker color for this plot."
edit-title="Manually set the line and marker color for this plot."
view-title="The line and marker color for this plot."
short-label="Color"
@colorSet="setColor"
/>
</ul>
</div>
</template>
<script>
import Color from "../../../../ui/color/Color";
import ColorPalette from "../../../../ui/color/ColorPalette";
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
export default {
components: { ColorSwatch },
inject: ['openmct', 'domainObject'],
data() {
return {
xKey: undefined,
yKey: undefined,
xKeyOptions: [],
yKeyOptions: [],
currentColor: undefined
};
},
mounted() {
this.plotSeries = [];
this.colorPalette = new ColorPalette();
this.initColor();
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
this.stopListening();
},
methods: {
initColor() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.styles = {
color
};
}
this.currentColor = this.domainObject.configuration.styles.color;
const colorObject = Color.fromHexString(this.currentColor);
this.colorPalette.remove(colorObject);
},
setColor(chosenColor) {
this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate(
this.domainObject,
`configuration.styles.color`,
this.currentColor
);
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setupOptions);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setupOptions();
},
removeSeries(seriesIdentifier) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
if (index >= 0) {
this.$delete(this.plotSeries, index);
this.setupOptions();
}
},
setupOptions() {
this.xKeyOptions = [];
this.yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
let update = false;
const series = this.plotSeries[0];
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
metadataValues.forEach((metadataValue) => {
this.xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
this.yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
});
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
} else {
this.xKey = undefined;
}
}
if (this.xKey === undefined) {
update = true;
xKeyOptionIndex = 0;
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
}
if (metadataValues.length > 1) {
if (this.domainObject.configuration.axes.yKey) {
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
} else {
this.yKey = undefined;
}
}
if (this.yKey === undefined) {
update = true;
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
}
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
if (index === xKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = yKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
}
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
if (index === yKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = xKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
if (update === true) {
this.saveConfiguration();
}
},
updateForm(property) {
if (property === 'xKey') {
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
if (xKeyOption.swap !== undefined) {
//swap
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
}
} else if (property === 'yKey') {
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
if (yKeyOption.swap !== undefined) {
//swap
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
}
}
this.saveConfiguration();
},
saveConfiguration() {
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
xKey: this.xKey,
yKey: this.yKey
});
}
}
};
</script>

View File

@ -0,0 +1,48 @@
import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants';
import Vue from 'vue';
import PlotOptions from "./PlotOptions.vue";
export default function ScatterPlotInspectorViewProvider(openmct) {
return {
key: SCATTER_PLOT_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object
&& object.type === SCATTER_PLOT_KEY;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlotOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<plot-options></plot-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -0,0 +1,127 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';
import ScatterPlotViewProvider from './ScatterPlotViewProvider';
import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider';
import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy';
import Vue from "vue";
import ScatterPlotForm from "./ScatterPlotForm.vue";
export default function () {
return function install(openmct) {
openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct));
openmct.types.addType(SCATTER_PLOT_KEY, {
key: SCATTER_PLOT_KEY,
name: "Scatter Plot",
cssClass: "icon-plot-scatter",
description: "View data as a scatter plot.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
styles: {},
axes: {},
ranges: {}
};
},
form: [
{
name: 'Underlay data (JSON file)',
key: 'selectFile',
control: 'file-input',
text: 'Select File...',
type: 'application/json',
removable: true,
hideFromInspector: true,
property: [
"selectFile"
]
},
{
name: "Underlay ranges",
control: "scatter-plot-form-control",
cssClass: "l-input",
key: "scatterPlotForm",
required: false,
hideFromInspector: false,
property: [
"configuration",
"ranges"
],
validate: ({ value }, callback) => {
const { rangeMin, rangeMax, domainMin, domainMax } = value;
const valid = {
rangeMin,
rangeMax,
domainMin,
domainMax
};
if (callback) {
callback(valid);
}
const values = Object.values(valid);
const hasAllValues = values.every(rangeValue => rangeValue !== undefined);
const hasNoValues = values.every(rangeValue => rangeValue === undefined);
return hasAllValues || hasNoValues;
}
}
],
priority: 891
});
openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));
openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);
};
function getScatterPlotFormControl(openmct) {
return {
show(element, model, onChange) {
const rowComponent = new Vue({
el: element,
components: {
ScatterPlotForm
},
provide: {
openmct
},
data() {
return {
model,
onChange
};
},
template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>`
});
return rowComponent;
}
};
}
}

View File

@ -0,0 +1,421 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {createOpenMct, resetApplicationState} from "utils/testing";
import Vue from "vue";
import ScatterPlotPlugin from "./plugin";
import ScatterPlot from './ScatterPlotView.vue';
import EventEmitter from "EventEmitter";
import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants';
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new ScatterPlotPlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The scatter plot view", () => {
let testDomainObject;
let scatterPlotObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
scatterPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.scatter-plot",
name: "Test Scatter Plot",
configuration: {
axes: {},
styles: {}
}
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
ScatterPlot
},
provide: {
openmct: openmct,
domainObject: scatterPlotObject,
composition: openmct.composition.get(scatterPlotObject)
},
template: "<ScatterPlot></ScatterPlot>"
});
await Vue.nextTick();
});
it("provides a scatter plot view", () => {
const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly scatter plot", () => {
let scatterPlotElement = element.querySelectorAll(".plotly");
expect(scatterPlotElement.length).toBe(1);
});
});
describe("the scatter plot objects", () => {
const mockObject = {
name: 'A very nice scatter plot',
key: SCATTER_PLOT_KEY,
creatable: true
};
it('defines a scatter plot object type with the correct key', () => {
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The scatter plot composition policy", () => {
it("allows composition for telemetry that contain at least 2 ranges", () => {
const parent = {
"composition": [],
"configuration": {
axes: {},
styles: {}
},
"name": "Some Scatter Plot",
"type": "telemetry.plot.scatter-plot",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain at least 2 range hints", () => {
const parent = {
"composition": [],
"configuration": {
axes: {},
styles: {}
},
"name": "Some Scatter Plot",
"type": "telemetry.plot.scatter-plot",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.scatter-plot",
configuration: {
axes: {},
styles: {
}
},
composition: [
{
key: '~Some~foo.scatter'
}
]
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-scatter-plot-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
});
});

View File

@ -0,0 +1,4 @@
export const SCATTER_PLOT_VIEW = 'scatter-plot.view';
export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';
export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';
export const TIME_STRIP_KEY = 'time-strip';

View File

@ -27,7 +27,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
{{ label }}
</div>
</component>
</template>
@ -39,28 +39,112 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
internalDomainObject: this.domainObject
conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
};
},
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
label() {
return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
;
}
},
watch: {
conditionSetIdentifier: {
handler(newValue, oldValue) {
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
return;
}
this.listenToConditionSetChanges();
},
deep: true
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
if (this.domainObject) {
this.updateDomainObject(this.domainObject);
this.listenToConditionSetChanges();
}
},
beforeDestroy() {
this.conditionSetIdentifier = null;
if (this.unlisten) {
this.unlisten();
}
this.stopListeningToConditionSetChanges();
},
methods: {
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
async listenToConditionSetChanges() {
if (!this.conditionSetIdentifier) {
return;
}
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
this.stopListeningToConditionSetChanges();
if (!conditionSetDomainObject) {
this.openmct.notifications.alert('Unable to find condition set');
}
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.updateConditionLabel, this);
this.telemetryCollection.load();
},
stopListeningToConditionSetChanges() {
if (this.telemetryCollection) {
this.telemetryCollection.off('add', this.updateConditionLabel, this);
this.telemetryCollection.destroy();
this.telemetryCollection = null;
}
},
updateConditionLabel([latestDatum]) {
if (!this.conditionSetIdentifier) {
this.stopListeningToConditionSetChanges();
return;
}
this.conditionalLabel = latestDatum.output || '';
},
updateDomainObject(domainObject) {
if (this.domainObjectLabel !== domainObject.label) {
this.domainObjectLabel = domainObject.label;
}
const urlDefined = domainObject.url && domainObject.url.length > 0;
if (this.urlDefined !== urlDefined) {
this.urlDefined = urlDefined;
}
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
if (this.url !== url) {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
}
}
}
};

View File

@ -222,20 +222,20 @@ export default {
.then(this.setObject);
}
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) {
this.removeSelectable();
}
this.openmct.time.off("bounds", this.refreshData);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
this.telemetryCollection.destroy();
if (this.mutablePromise) {
this.mutablePromise.then(() => {
@ -253,34 +253,9 @@ export default {
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
},
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
this.updateView();
},
updateView() {
if (!this.updatingView) {
@ -291,17 +266,10 @@ export default {
});
}
},
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) {
if (!isTick) {
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},
setObject(domainObject) {
@ -315,8 +283,13 @@ export default {
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.requestHistoricalData();
this.subscribeToObject();
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);
this.telemetryCollection.load();
this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject);

View File

@ -57,7 +57,7 @@
/>
<drop-hint
:key="i"
:key="'hint-' + i"
class="c-fl-frame__drop-hint"
:index="i"
:allow-drop="allowDrop"
@ -66,7 +66,7 @@
<resize-handle
v-if="(i !== frames.length - 1)"
:key="i"
:key="'handle-' + i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
:is-editing="isEditing"

View File

@ -1,5 +1,5 @@
<template>
<div class="c-table c-table--sortable c-list-view">
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header c-list-view--selectable">
<table class="c-table__body">
<thead class="c-table__header">
<tr>

View File

@ -1,32 +0,0 @@
/******************************* LIST VIEW */
.c-list-view {
overflow-x: auto !important;
overflow-y: auto;
tbody tr {
background: $colorListItemBg;
transition: $transOut;
}
body.desktop & {
tbody tr {
cursor: pointer;
&:hover {
background: $colorListItemBgHov;
filter: $filterHov;
transition: $transIn;
}
}
}
td {
$p: floor($interiorMargin * 1.5);
@include ellipsize();
line-height: 120%; // Needed for icon alignment
max-width: 0;
padding-top: $p;
padding-bottom: $p;
width: 25%;
}
}

View File

@ -0,0 +1,128 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import CreateAction from './CreateAction';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { debounce } from 'lodash';
let parentObject;
let parentObjectPath;
let unObserve;
describe("The create action plugin", () => {
let openmct;
const TYPES = [
'clock',
'conditionWidget',
'conditionWidget',
'example.imagery',
'example.state-generator',
'flexible-layout',
'folder',
'generator',
'hyperlink',
'LadTable',
'LadTableSet',
'layout',
'mmgis',
'notebook',
'plan',
'table',
'tabs',
'telemetry-mean',
'telemetry.plot.bar-graph',
'telemetry.plot.overlay',
'telemetry.plot.stacked',
'time-strip',
'timer',
'webpage'
];
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('creates new objects for a', () => {
beforeEach(() => {
parentObject = {
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
composition: []
};
parentObjectPath = [parentObject];
spyOn(openmct.objects, 'save');
openmct.objects.save.and.callThrough();
spyOn(openmct.forms, 'showForm');
openmct.forms.showForm.and.callFake(formStructure => {
return Promise.resolve({
name: 'test',
notes: 'test notes',
location: parentObjectPath
});
});
});
afterEach(() => {
parentObject = null;
unObserve();
});
TYPES.forEach(type => {
it(`type ${type}`, (done) => {
function callback(newObject) {
const composition = newObject.composition;
openmct.objects.get(composition[0])
.then(object => {
expect(object.type).toEqual(type);
expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier));
done();
});
}
const deBouncedCallback = debounce(callback, 300);
unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);
const createAction = new CreateAction(openmct, type, parentObject);
createAction.invoke();
});
});
});
});

View File

@ -90,6 +90,9 @@ export default class CreateWizard {
rows: this.properties.map(property => {
const row = JSON.parse(JSON.stringify(property));
row.value = this.getValue(row);
if (property.validate) {
row.validate = property.validate;
}
return row;
}).filter(row => row && row.control)

View File

@ -45,47 +45,35 @@ export default class EditPropertiesAction extends PropertiesAction {
}
invoke(objectPath) {
this._showEditForm(objectPath);
return this._showEditForm(objectPath);
}
/**
* @private
*/
async _onSave(changes) {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
_onSave(changes) {
try {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
object = value;
this.openmct.objects.mutate(this.domainObject, key, value);
this.openmct.notifications.info('Save successful');
});
object = value;
});
this.domainObject.modified = Date.now();
// Show saving progress dialog
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 'unknown',
message: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
iconClass: 'info',
title: 'Saving'
});
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
this.openmct.notifications.info('Save successful');
} else {
} catch (error) {
this.openmct.notifications.error('Error saving objects');
console.error(error);
}
dialog.dismiss();
}
/**
@ -98,7 +86,7 @@ export default class EditPropertiesAction extends PropertiesAction {
const formStructure = createWizard.getFormStructure(false);
formStructure.title = 'Edit ' + this.domainObject.name;
this.openmct.forms.showForm(formStructure)
return this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
}
}

View File

@ -0,0 +1,222 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createMouseEvent,
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { debounce } from 'lodash';
describe('EditPropertiesAction plugin', () => {
let editPropertiesAction;
let openmct;
let element;
beforeEach((done) => {
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless(element);
editPropertiesAction = openmct.actions.getAction('properties');
});
afterEach(() => {
editPropertiesAction = null;
return resetApplicationState(openmct);
});
it('editPropertiesAction exists', () => {
expect(editPropertiesAction.key).toEqual('properties');
});
it('edit properties action applies to only persistable objects', () => {
spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
const domainObject = {
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
composition: []
};
const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
expect(isApplicableTo).toBe(true);
});
it('edit properties action does not apply to non persistable objects', () => {
spyOn(openmct.objects, 'isPersistable').and.returnValue(false);
const domainObject = {
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
composition: []
};
const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
expect(isApplicableTo).toBe(false);
});
it('edit properties action when invoked shows form', (done) => {
const domainObject = {
name: 'mock folder',
notes: 'mock notes',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
modified: 1643065068597,
persisted: 1643065068600,
composition: []
};
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
function handleFormPropertyChange(data) {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
expect(title.value).toEqual(domainObject.name);
const notes = form.querySelector('textArea');
expect(notes.value).toEqual(domainObject.notes);
const buttons = form.querySelectorAll('button');
expect(buttons[0].textContent.trim()).toEqual('OK');
expect(buttons[1].textContent.trim()).toEqual('Cancel');
const clickEvent = createMouseEvent('click');
buttons[1].dispatchEvent(clickEvent);
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
editPropertiesAction.invoke([domainObject])
.catch(() => {
done();
});
});
it('edit properties action saves changes', (done) => {
const oldName = 'mock folder';
const newName = 'renamed mock folder';
const domainObject = {
name: oldName,
notes: 'mock notes',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
modified: 1643065068597,
persisted: 1643065068600,
composition: []
};
let unObserve;
function callback(newObject) {
expect(newObject.name).not.toEqual(oldName);
expect(newObject.name).toEqual(newName);
unObserve();
done();
}
const deBouncedCallback = debounce(callback, 300);
unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
let changed = false;
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
function handleFormPropertyChange(data) {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
const notes = form.querySelector('textArea');
const buttons = form.querySelectorAll('button');
expect(buttons[0].textContent.trim()).toEqual('OK');
expect(buttons[1].textContent.trim()).toEqual('Cancel');
if (!changed) {
expect(title.value).toEqual(domainObject.name);
expect(notes.value).toEqual(domainObject.notes);
// change input field value and dispatch event for it
title.focus();
title.value = newName;
title.dispatchEvent(new Event('input'));
title.blur();
changed = true;
} else {
// click ok to save form changes
const clickEvent = createMouseEvent('click');
buttons[0].dispatchEvent(clickEvent);
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
}
editPropertiesAction.invoke([domainObject]);
});
it('edit properties action discards changes', (done) => {
const name = 'mock folder';
const domainObject = {
name,
notes: 'mock notes',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
},
modified: 1643065068597,
persisted: 1643065068600,
composition: []
};
editPropertiesAction.invoke([domainObject])
.catch(() => {
expect(domainObject.name).toEqual(name);
done();
});
const form = document.querySelector('.js-form');
const buttons = form.querySelectorAll('button');
const clickEvent = createMouseEvent('click');
buttons[1].dispatchEvent(clickEvent);
});
});

View File

@ -0,0 +1,199 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GaugeViewProvider from './GaugeViewProvider';
import GaugeFormController from './components/GaugeFormController.vue';
import Vue from 'vue';
export const GAUGE_TYPES = [
['Filled Dial', 'dial-filled'],
['Needle Dial', 'dial-needle'],
['Vertical Meter', 'meter-vertical'],
['Vertical Meter Inverted', 'meter-vertical-inverted'],
['Horizontal Meter', 'meter-horizontal']
];
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
openmct.types.addType('gauge', {
name: "Gauge",
creatable: true,
description: "Graphically visualize a telemetry element's current value between a minimum and maximum.",
cssClass: 'icon-gauge',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
gaugeController: {
gaugeType: GAUGE_TYPES[0][1],
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
max: 100,
min: 0,
precision: 2
}
};
},
form: [
{
name: "Display current value",
control: "toggleSwitch",
cssClass: "l-input",
key: "isDisplayCurVal",
property: [
"configuration",
"gaugeController",
"isDisplayCurVal"
]
},
{
name: "Display range values",
control: "toggleSwitch",
cssClass: "l-input",
key: "isDisplayMinMax",
property: [
"configuration",
"gaugeController",
"isDisplayMinMax"
]
},
{
name: "Float precision",
control: "numberfield",
cssClass: "l-input-sm",
key: "precision",
property: [
"configuration",
"gaugeController",
"precision"
]
},
{
name: "Gauge type",
options: GAUGE_TYPES.map(type => {
return {
name: type[0],
value: type[1]
};
}),
control: "select",
cssClass: "l-input-sm",
key: "gaugeController",
property: [
"configuration",
"gaugeController",
"gaugeType"
]
},
{
name: "Value ranges and limits",
control: "gauge-controller",
cssClass: "l-input",
key: "gaugeController",
required: false,
hideFromInspector: true,
property: [
"configuration",
"gaugeController"
],
validate: ({ value }, callback) => {
if (value.isUseTelemetryLimits) {
return true;
}
const { min, max, limitLow, limitHigh } = value;
const valid = {
min: true,
max: true,
limitLow: true,
limitHigh: true
};
if (min === '') {
valid.min = false;
}
if (max === '') {
valid.max = false;
}
if (max < min) {
valid.min = false;
valid.max = false;
}
if (limitLow !== '') {
valid.limitLow = min <= limitLow && limitLow < max;
}
if (limitHigh !== '') {
valid.limitHigh = min < limitHigh && limitHigh <= max;
}
if (valid.limitLow && valid.limitHigh
&& limitLow !== '' && limitHigh !== ''
&& limitLow > limitHigh) {
valid.limitLow = false;
valid.limitHigh = false;
}
if (callback) {
callback(valid);
}
return valid.min && valid.max && valid.limitLow && valid.limitHigh;
}
}
]
});
};
function getGaugeFormController(openmct) {
return {
show(element, model, onChange) {
const rowComponent = new Vue({
el: element,
components: {
GaugeFormController
},
provide: {
openmct
},
data() {
return {
model,
onChange
};
},
template: `<GaugeFormController :model="model" @onChange="onChange"></GaugeFormController>`
});
return rowComponent;
}
};
}
}

View File

@ -0,0 +1,801 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { debounce } from 'lodash';
import Vue from 'vue';
let gaugeDomainObject = {
identifier: {
key: 'gauge',
namespace: 'test-namespace'
},
type: 'gauge',
composition: []
};
describe('Gauge plugin', () => {
let openmct;
let child;
let gaugeHolder;
beforeEach((done) => {
gaugeHolder = document.createElement('div');
gaugeHolder.style.display = 'block';
gaugeHolder.style.width = '1920px';
gaugeHolder.style.height = '1080px';
child = document.createElement('div');
gaugeHolder.appendChild(child);
openmct = createOpenMct();
openmct.on('start', done);
openmct.install(openmct.plugins.Gauge());
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('Plugin installed by default', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType).not.toBeNull();
expect(gaugueType.definition.name).toEqual('Gauge');
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
});
it('Gaugue form controller', () => {
const gaugeController = openmct.forms.getFormControl('gauge-controller');
expect(gaugeController).toBeDefined();
});
describe('Gaugue with Filled Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Needle Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-needle',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Vertical Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-meter-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Vertical Meter Inverted', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
beforeEach(() => {
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: 1,
min: -1,
precision: 2
}
},
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
});
describe('Gaugue with Horizontal Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
beforeEach(() => {
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: 1,
min: -1,
precision: 2
}
},
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
expect(hasMajorElements).toBe(true);
});
});
describe('Gaugue with Filled Dial with Use Telemetry Limits', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
max: 100,
min: 0,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue(
{
limits: () => Promise.resolve({
CRITICAL: {
high: 0.99,
low: -0.99
}
})
}
);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
});

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GaugeComponent from './components/Gauge.vue';
import Vue from 'vue';
export default function GaugeViewProvider(openmct) {
return {
key: 'gauge',
name: 'Gauge',
cssClass: 'icon-gauge',
canView: function (domainObject) {
return domainObject.type === 'gauge';
},
canEdit: function (domainObject) {
if (domainObject.type === 'gauge') {
return true;
}
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
GaugeComponent
},
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject)
},
template: '<gauge-component></gauge-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -0,0 +1,566 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
>
<template v-if="typeDial">
<svg
width="0"
height="0"
class="c-dial__clip-paths"
>
<defs>
<clipPath
id="gaugeBgMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
</clipPath>
<clipPath
id="gaugeValueMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
</clipPath>
</defs>
</svg>
<svg
class="c-dial__range c-gauge__range js-gauge-dial-range"
viewBox="0 0 512 512"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<svg
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox"
>
<text
class="c-dial__current-value-text js-dial-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</svg>
<svg
class="c-dial__bg"
viewBox="0 0 10 10"
>
<g
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
class="c-dial__limit-low"
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
class="c-dial__low-limit__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
class="c-dial__high-limit__low"
x="0"
y="5"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="typeFilledDial"
class="c-dial__filled-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__filled-value"
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="degValue >= getLimitDegree('low', 'q1')"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q2')"
class="c-dial__filled-value__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q3')"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
</svg>
</template>
<template v-if="typeMeter">
<div class="c-meter">
<div
v-if="displayMinMax"
class="c-gauge__range c-meter__range js-gauge-meter-range"
>
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
>
<text
class="c-dial__current-value-text js-meter-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</svg>
</div>
</div>
</template>
</div>
</template>
<script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10;
export default {
name: 'Gauge',
inject: ['openmct', 'domainObject', 'composition'],
data() {
let gaugeController = this.domainObject.configuration.gaugeController;
return {
curVal: 0,
digits: 3,
precision: gaugeController.precision,
displayMinMax: gaugeController.isDisplayMinMax,
displayCurVal: gaugeController.isDisplayCurVal,
limitHigh: gaugeController.limitHigh,
limitLow: gaugeController.limitLow,
rangeHigh: gaugeController.max,
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
activeTimeSystem: this.openmct.time.timeSystem()
};
},
computed: {
degValue() {
return this.percentToDegrees(this.valToPercent(this.curVal));
},
degValueFilledDial() {
if (this.curVal > this.rangeHigh) {
return this.percentToDegrees(100);
}
return this.percentToDegrees(this.valToPercent(this.curVal));
},
dialHighLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitHigh));
},
dialLowLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitLow));
},
curValViewBox() {
const DIGITS_RATIO = 10;
const VIEWBOX_STR = '0 0 X 15';
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
typeDial() {
return this.matchGaugeType('dial');
},
typeFilledDial() {
return this.matchGaugeType('dial-filled');
},
typeNeedleDial() {
return this.matchGaugeType('dial-needle');
},
typeMeter() {
return this.matchGaugeType('meter');
},
typeMeterHorizontal() {
return this.matchGaugeType('horizontal');
},
typeMeterVertical() {
return this.matchGaugeType('vertical');
},
typeMeterInverted() {
return this.matchGaugeType('inverted');
},
meterValueToPerc() {
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
if (this.curVal <= this.rangeLow) {
return meterDirection * 100;
}
if (this.curVal >= this.rangeHigh) {
return 0;
}
return this.valToPercentMeter(this.curVal) * meterDirection;
},
meterHighLimitPerc() {
return this.valToPercentMeter(this.limitHigh);
},
meterLowLimitPerc() {
return 100 - this.valToPercentMeter(this.limitLow);
},
valueInBounds() {
return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh);
},
timeFormatter() {
const timeSystem = this.activeTimeSystem;
const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
return this.openmct.telemetry.getValueFormatter(metadataValue);
}
},
watch: {
curVal(newCurValue) {
if (this.digits < newCurValue.toString().length) {
this.digits = newCurValue.toString().length;
}
}
},
mounted() {
this.composition.on('add', this.addedToComposition);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('timeSystem', this.setTimeSystem);
},
destroyed() {
this.composition.off('add', this.addedToComposition);
this.composition.off('remove', this.removeTelemetryObject);
if (this.unsubscribe) {
this.unsubscribe();
}
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.setTimeSystem);
},
methods: {
getLimitDegree: getLimitDegree,
addTelemetryObjectAndSubscribe(domainObject) {
this.telemetryObject = domainObject;
this.request();
this.subscribe();
},
addedToComposition(domainObject) {
if (this.telemetryObject) {
this.confirmRemoval(domainObject);
} else {
this.addTelemetryObjectAndSubscribe(domainObject);
}
},
confirmRemoval(domainObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
this.removeFromComposition();
this.removeTelemetryObject();
this.addTelemetryObjectAndSubscribe(domainObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(domainObject);
dialog.dismiss();
}
}
]
});
},
matchGaugeType(str) {
return this.gaugeType.indexOf(str) !== -1;
},
percentToDegrees(vPercent) {
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
refreshData(bounds, isTick) {
if (!isTick) {
this.request();
}
},
removeTelemetryObject() {
if (this.unsubscribe) {
this.unsubscribe();
this.unsubscribe = null;
}
this.metadata = null;
this.formats = null;
this.valueKey = null;
this.limitHigh = null;
this.limitLow = null;
this.rangeHigh = null;
this.rangeLow = null;
},
request(domainObject = this.telemetryObject) {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
LimitEvaluator.limits().then(this.updateLimits);
this.valueKey = this
.metadata
.valuesForHints(['range'])[0].source;
this.openmct
.telemetry
.request(domainObject, { strategy: 'latest' })
.then(values => {
const length = values.length;
this.updateValue(values[length - 1]);
});
},
round(val, decimals = this.precision) {
let precision = Math.pow(10, decimals);
return Math.round(val * precision) / precision;
},
setTimeSystem(timeSystem) {
this.activeTimeSystem = timeSystem;
},
subscribe(domainObject = this.telemetryObject) {
this.unsubscribe = this.openmct
.telemetry
.subscribe(domainObject, this.updateValue.bind(this));
},
updateLimits(telemetryLimit) {
if (!telemetryLimit || !this.domainObject.configuration.gaugeController.isUseTelemetryLimits) {
return;
}
let limits = {
high: 0,
low: 0
};
if (telemetryLimit.CRITICAL) {
limits = telemetryLimit.CRITICAL;
} else if (telemetryLimit.DISTRESS) {
limits = telemetryLimit.DISTRESS;
} else if (telemetryLimit.SEVERE) {
limits = telemetryLimit.SEVERE;
} else if (telemetryLimit.WARNING) {
limits = telemetryLimit.WARNING;
} else if (telemetryLimit.WATCH) {
limits = telemetryLimit.WATCH;
} else {
this.openmct.notifications.error('No limits definition for given telemetry');
}
this.limitHigh = this.round(limits.high[this.valueKey]);
this.limitLow = this.round(limits.low[this.valueKey]);
this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100);
this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100));
},
updateValue(datum) {
this.datum = datum;
if (this.isRendering) {
return;
}
const { start, end } = this.openmct.time.bounds();
const parsedValue = this.timeFormatter.parse(this.datum);
const beforeStartOfBounds = parsedValue < start;
const afterEndOfBounds = parsedValue > end;
if (afterEndOfBounds || beforeStartOfBounds) {
return;
}
this.isRendering = true;
requestAnimationFrame(() => {
this.isRendering = false;
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
});
},
valToPercent(vValue) {
// Used by dial
if (vValue >= this.rangeHigh && this.typeFilledDial) {
// For filled dial, clip values over the high range to prevent over-rotation
return 100;
}
return ((vValue - this.rangeLow) / (this.rangeHigh - this.rangeLow)) * 100;
},
valToPercentMeter(vValue) {
return this.round((this.rangeHigh - vValue) / (this.rangeHigh - this.rangeLow) * 100, 2);
}
}
};
</script>

View File

@ -0,0 +1,171 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<span class="form-control">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
:id="'gaugeToggle'"
:checked="isUseTelemetryLimits"
label="Use telemetry limits for minimum and maximum ranges"
@change="toggleUseTelemetryLimits"
/>
<div
v-if="!isUseTelemetryLimits"
class="c-form--sub-grid"
>
<div class="c-form__row">
<span class="req-indicator req">
</span>
<label>Range minimum value</label>
<input
ref="min"
v-model.number="min"
data-field-name="min"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator">
</span>
<label>Range low limit</label>
<input
ref="limitLow"
v-model.number="limitLow"
data-field-name="limitLow"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator req">
</span>
<label>Range maximum value</label>
<input
ref="max"
v-model.number="max"
data-field-name="max"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator">
</span>
<label>Range high limit</label>
<input
ref="limitHigh"
v-model.number="limitHigh"
data-field-name="limitHigh"
type="number"
@input="onChange"
>
</div>
</div>
</span>
</span>
</template>
<script>
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
export default {
components: {
ToggleSwitch
},
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
min: this.model.value.min
};
},
methods: {
onChange(event) {
const data = {
model: this.model,
value: {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,
max: this.max,
min: this.min,
precision: this.model.value.precision
}
};
if (event) {
const target = event.target;
const targetIndicator = target.parentElement.querySelector('.req-indicator');
if (targetIndicator.classList.contains('req')) {
targetIndicator.classList.add('visited');
}
this.model.validate(data, (valid) => {
Object.entries(valid).forEach(([key, isValid]) => {
const element = this.$refs[key];
const reqIndicatorElement = element.parentElement.querySelector('.req-indicator');
reqIndicatorElement.classList.toggle('invalid', !isValid);
if (reqIndicatorElement.classList.contains('req') && (!isValid || reqIndicatorElement.classList.contains('visited'))) {
reqIndicatorElement.classList.toggle('valid', isValid);
}
});
});
}
this.$emit('onChange', data);
},
toggleUseTelemetryLimits() {
this.isUseTelemetryLimits = !this.isUseTelemetryLimits;
this.onChange();
},
toggleMinMax() {
this.isDisplayMinMax = !this.isDisplayMinMax;
this.onChange();
}
}
};
</script>

View File

@ -0,0 +1,39 @@
const GAUGE_LIMITS = {
q1: 0,
q2: 90,
q3: 180,
q4: 270
};
export const DIAL_VALUE_DEG_OFFSET = 45;
// type: low, high
// quadrant: low, mid, high, max
export function getLimitDegree(type, quadrant) {
if (quadrant === 'max') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
return type === 'low'
? getLowLimitDegree(quadrant)
: getHighLimitDegree(quadrant)
;
}
function getLowLimitDegree(quadrant) {
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
}
function getHighLimitDegree(quadrant) {
if (quadrant === 'q1') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q2') {
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q3') {
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
}
}

View File

@ -0,0 +1,223 @@
.is-object-type-gauge {
overflow: hidden;
}
.req-indicator {
width: 20px;
&.invalid,
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
&.valid,
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
}
.c-gauge {
// Both dial and meter types
overflow: hidden;
&__range {
$c: $colorGaugeRange;
color: $c;
text {
fill: $c;
}
}
&__wrapper {
@include abs();
overflow: hidden;
}
}
/********************************************** DIAL GAUGE */
svg[class*='c-dial'] {
max-height: 100%;
max-width: 100%;
position: absolute;
g {
transform-origin: center;
}
}
.c-dial {
&__bg {
background: $colorGaugeBg;
clip-path: url(#gaugeBgMask);
}
&__limit-high rect { fill: $colorGaugeLimitHigh; }
&__limit-low rect { fill: $colorGaugeLimitLow; }
&__filled-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__needle-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__filled-value { fill: $colorGaugeValue; }
&__needle-value {
fill: $colorGaugeValue;
transition: transform $transitionTimeGauge;
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
}
/********************************************** METER GAUGE */
.c-meter {
// Common styles for c-meter
@include abs();
display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range {
display: flex;
flex: 0 0 auto;
justify-content: space-between;
}
&__bg {
background: $colorGaugeBg;
border-radius: $basicCr;
flex: 1 1 auto;
overflow: hidden;
}
&__value {
// Filled area
position: absolute;
background: $colorGaugeValue;
transition: transform $transitionTimeGauge;
z-index: 1;
}
.c-gauge__curval {
fill: $colorGaugeMeterTextValue !important;
}
[class*='limit'] {
position: absolute;
}
&__limit-high {
background: $colorGaugeLimitHigh;
}
&__limit-low {
background: $colorGaugeLimitLow;
}
}
.c-meter {
.c-gauge--meter-vertical &,
.c-gauge--meter-vertical-inverted & {
&__range {
flex-direction: column;
min-width: min-content;
margin-right: $interiorMarginSm;
text-align: right;
}
&__value {
// Filled area
$lrM: $marginGaugeMeterValue;
left: $lrM;
right: $lrM;
top: 0;
bottom: 0;
}
[class*='limit'] {
left: 0;
right: 0;
}
}
.c-gauge--meter-vertical & {
&__limit-low {
bottom: 0;
}
&__limit-high {
top: 0;
}
}
.c-gauge--meter-vertical-inverted & {
&__limit-low {
top: 0;
}
&__limit-high {
bottom: 0;
}
&__range__low {
order: 1;
}
&__range__high {
order: 2;
}
}
.c-gauge--meter-horizontal & {
flex-direction: column;
&__range {
flex-direction: row;
min-height: min-content;
margin-top: $interiorMarginSm;
order: 2;
&__high {
order: 2;
}
&__low {
order: 1;
}
}
&__bg {
order: 1;
}
&__value {
// Filled area
$m: $marginGaugeMeterValue;
top: $m;
bottom: $m;
left: 0;
right: 0;
}
[class*='limit'] {
top: 0;
bottom: 0;
}
&__limit-low {
left: 0;
}
&__limit-high {
right: 0;
}
}
}

View File

@ -2,11 +2,16 @@ import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
const DEFAULT_IMAGE_FRESHNESS_OPTIONS = {
fadeOutDelayTime: '0s',
fadeOutDurationTime: '30s'
};
export default class ImageryView {
constructor(openmct, domainObject, objectPath) {
constructor(openmct, domainObject, objectPath, options) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.options = options;
this.component = undefined;
}
@ -27,6 +32,7 @@ export default class ImageryView {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: alternateObjectPath || this.objectPath,
imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS,
currentView: this
},
data() {

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