Compare commits

...

55 Commits

Author SHA1 Message Date
bfa604efa0 Condition Widgets trigger hundreds of persistence calls. #5137 2022-04-29 02:03:23 -07:00
e75befafbd 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>
2022-04-28 14:03:35 -07:00
d7d06b59ea Gauge edit enabled 2.0.3 (#5133)
* Gauge plugin #4896, add edit mode
2022-04-28 13:32:12 -07:00
8b4a55a7ec 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>
2022-04-28 12:43:30 -07:00
2519e601d7 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
2022-04-28 12:03:42 -07:00
b7b205621b added telemetry collection to alphanumeric telemetry view (#5131) 2022-04-28 11:35:35 -07:00
3d2d932323 [LAD Tables] Use Telemetry Collections (#5127)
* Use telemetry collections to handle bounds checks
2022-04-28 11:21:58 -07:00
ce28dd2b9f 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
2022-04-28 10:44:53 -07:00
286a533dad 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.
2022-04-26 13:40:19 -07:00
378a4ca282 Release 2.0.3 2022-04-25 11:58:50 -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
43afb39e56 Added soft assertion for exampleImagery e2e (#5021) 2022-03-30 14:16:59 -07:00
cd8c332fb5 Bump @types/jasmine from 3.10.4 to 4.0.1 (#5018)
Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 3.10.4 to 4.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-30 21:11:51 +00:00
b899475939 Bump mini-css-extract-plugin from 2.4.5 to 2.6.0 (#4926)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.5 to 2.6.0.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.5...v2.6.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2022-03-30 13:03:12 -07:00
cc1f7659f9 Bump sass-loader from 12.4.0 to 12.6.0 (#4871)
* Bump sass-loader from 12.4.0 to 12.6.0

Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.4.0 to 12.6.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.4.0...v12.6.0)

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

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

* update sass": "1.49.9"

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 <nikhil.k.mandlik@nasa.gov>
2022-03-30 18:04:40 +00:00
0d5539be96 Bump moment-timezone from 0.5.28 to 0.5.34 (#5005)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.28 to 0.5.34.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.28...0.5.34)

---
updated-dependencies:
- dependency-name: moment-timezone
  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-03-30 11:00:07 -07:00
0a511e6155 remove file loader as dependency (#5012) 2022-03-30 18:58:26 +02:00
47b6d19de8 Bump webpack-dev-middleware from 3.7.3 to 5.3.1 (#4829)
* Bump webpack-dev-middleware from 3.7.3 to 5.3.1

Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 3.7.3 to 5.3.1.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v3.7.3...v5.3.1)

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

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

* Fixed config to support updated webpack-dev-middleware

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: Andrew Henry <akhenry@gmail.com>
2022-03-30 00:58:49 +00:00
3fd93f47bc Remove html-loader (#4991)
* Bump html-loader from 0.5.5 to 3.1.0

Bumps [html-loader](https://github.com/webpack-contrib/html-loader) from 0.5.5 to 3.1.0.
- [Release notes](https://github.com/webpack-contrib/html-loader/releases)
- [Changelog](https://github.com/webpack-contrib/html-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/html-loader/compare/v0.5.5...v3.1.0)

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

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

* fix asset loading and make clean idempotent

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-03-29 23:11:15 +00:00
651e61954c Type annotations (#4789)
* add some types to XAxisModel

* some more type defs and small code tweaks while getting familiar with plots

* more type annotations and a few small tweaks

* more type annotations and small tweaks to make types show

* add mocha types

* Add karma and jasmine, too

* further simplify plot canvas creation

* further simplify plot canvas creation

* update types, avoid runtime behavior in type definition that breaks SeriesCollection

* undo the changes to MctChart, improve it later

* lint fix

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-03-29 14:39:49 -07:00
d30ec4c757 Bump sass from 1.49.0 to 1.49.9 (#4985)
Bumps [sass](https://github.com/sass/dart-sass) from 1.49.0 to 1.49.9.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.49.0...1.49.9)

---
updated-dependencies:
- dependency-name: sass
  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: Andrew Henry <andrew.k.henry@nasa.gov>
2022-03-28 21:01:11 -07:00
3c24733476 Update package.json (#5004) 2022-03-28 17:32:05 -07:00
04d00fac3d relax text assertion to be any number (#5001) 2022-03-28 11:20:55 -07:00
150909d4b9 [Build] Remove testing and support for node12 (#4963)
* Remove all node12 testing other than platform

* add lighthouse to our deps now that 12 is deprecated

* removing 12 from platform pr

* Updated config.yml

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-03-25 18:11:41 +00:00
2b599a7ff4 Fix Cursor Grab while panning (#4957)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-03-25 11:07:05 -07:00
824a597825 remove raw-loader as a dependency (#4998)
* remove raw-loader as a dependency

* move clock to correct dir and response to canvas clicking
2022-03-25 09:13:10 -07:00
126 changed files with 6225 additions and 865 deletions

View File

@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.19.1-focal
- image: mcr.microsoft.com/playwright:v1.19.2-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
@ -101,7 +101,7 @@ jobs:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
@ -142,21 +142,22 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node16-lint
node-version: lts/gallium
- unit-test:
name: node12-chrome
node-version: lts/erbium
browser: ChromeHeadless
- unit-test:
name: node14-chrome
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
@ -164,13 +165,9 @@ workflows:
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
name: node16-firefoxESR-nightly
node-version: lts/gallium
browser: FirefoxESR
- unit-test:
name: node12-chrome-nightly
node-version: lts/erbium
browser: ChromeHeadless
- unit-test:
name: node14-firefox-nightly
node-version: lts/fermium
@ -183,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

@ -34,7 +34,7 @@ jobs:
- run: npm 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

@ -9,8 +9,6 @@ on:
pull_request:
types:
- labeled
schedule:
- cron: '28 21 * * 1-5'
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
@ -20,10 +18,10 @@ jobs:
uses: actions/checkout@v3
with:
ref: master #explicitly checkout master for baseline
- name: Install Node 14
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
@ -54,10 +52,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node 14
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
@ -83,9 +81,9 @@ jobs:
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:

View File

@ -16,9 +16,9 @@ jobs:
- macos-latest
- windows-latest
node_version:
- 12
- 14
- 16
- 18
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}

2
app.js
View File

@ -64,7 +64,7 @@ app.use(require('webpack-dev-middleware')(
compiler,
{
publicPath: '/dist',
logLevel: 'warn'
stats: 'errors-warnings'
}
));

View File

@ -157,5 +157,10 @@ test.describe('Sine Wave Generator', () => {
y: 28
}
});
// Verify that where we click on canvas shows the number we clicked on
// Note that any number will do, we just care that a number exists
await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
});
});

View File

@ -166,17 +166,17 @@ test.describe('Example Imagery', () => {
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox();
expect(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
expect(resetBoundingBox.height).toEqual(initialBoundingBox.height);
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
});

View File

@ -0,0 +1,190 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Test for plot autoscale.
*/
const { test: _test, 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('autoscale off causes no error from undefined user range', async ({ page }) => {
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 }))
]);
let errorCount = 0;
function onError() {
errorCount++;
}
page.on('pageerror', onError);
await page.keyboard.down('Alt');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 200,
y: 200
},
targetPosition: {
x: 400,
y: 400
}
});
await page.keyboard.up('Alt');
page.off('pageerror', onError);
// There would have been an error at this point. So if there isn't, then
// we fixed it.
expect(errorCount).toBe(0);
// 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(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=OK').click()
]);
// 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(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
page.locator('text=OK').click()
]);
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
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 Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
// save
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();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testYTicks(page, values) {
const yTicks = page.locator('.gl-plot-y-tick-label');
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: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,279 @@
/*****************************************************************************
* 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.
*/
const { test, expect } = require('@playwright/test');
test.describe('Log plot tests', () => {
test.only('Can create a log plot.', async ({ page }) => {
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
await page.reload();
// test log ticks hold up after refresh
await testLogTicks(page);
await testLogPlotPixels(page);
});
test.only('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(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=OK').click()
]);
// save the overlay plot
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();
// 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(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/),
page.locator('text=OK').click()
]);
// click on overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testRegularTicks(page) {
const yTicks = page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(7);
await expect(yTicks.nth(0)).toHaveText('-2');
await expect(yTicks.nth(1)).toHaveText('0');
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 = 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();
}
/**
* @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 page.locator('text=Save and Finish Editing').click();
}
/**
* @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, 50));
// 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

@ -35,8 +35,8 @@ define([
phase: 0
};
function GeneratorProvider() {
this.workerInterface = new WorkerInterface();
function GeneratorProvider(openmct) {
this.workerInterface = new WorkerInterface(openmct);
}
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {

View File

@ -21,20 +21,13 @@
*****************************************************************************/
define([
'raw-loader!./generatorWorker.js',
'uuid'
], function (
workerText,
uuid
) {
var workerBlob = new Blob(
[workerText],
{type: 'application/javascript'}
);
var workerUrl = URL.createObjectURL(workerBlob);
function WorkerInterface() {
function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};

View File

@ -146,7 +146,7 @@ define([
}
});
openmct.telemetry.addProvider(new GeneratorProvider());
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
};

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

@ -38,6 +38,10 @@ module.exports = (config) => {
{
pattern: 'dist/inMemorySearchWorker.js*',
included: false
},
{
pattern: 'dist/generatorWorker.js*',
included: false
}
],
port: 9876,
@ -92,8 +96,7 @@ module.exports = (config) => {
},
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-only',
logLevel: 'warn'
stats: 'errors-warnings'
},
concurrency: 1,
singleRun: true,

View File

@ -1,25 +1,29 @@
{
"name": "openmct",
"version": "2.0.2-SNAPSHOT",
"version": "2.0.3",
"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",
"@percy/cli": "1.0.4",
"@percy/playwright": "1.0.2",
"@playwright/test": "1.19.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"allure-playwright": "2.0.0-beta.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",
"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",
"eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.8.0",
"eslint-plugin-vue": "8.5.0",
@ -27,15 +31,13 @@
"eventemitter3": "1.2.0",
"exports-loader": "0.7.0",
"express": "4.13.1",
"file-loader": "6.2.0",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html-loader": "0.5.5",
"html2canvas": "1.4.1",
"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",
@ -44,40 +46,40 @@
"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.12",
"mini-css-extract-plugin": "2.4.5",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
"moment": "2.29.1",
"moment-duration-format": "2.2.2",
"moment-timezone": "0.5.28",
"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",
"printj": "1.3.1",
"raw-loader": "4.0.2",
"request": "2.88.2",
"resolve-url-loader": "4.0.0",
"sass": "1.49.0",
"sass-loader": "12.4.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",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "3.7.3",
"webpack-dev-middleware": "5.3.1",
"webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules; rm package-lock.json",
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "node app.js",
"lint": "eslint example src --ext .js,.vue openmct.js",
@ -92,6 +94,7 @@
"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:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:debug": "npm run test:e2e:local -- --debug",
"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",
@ -107,7 +110,10 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=12.22.0"
"node": ">=14.19.1"
},
"overrides": {
"core-js": "3.21.1"
},
"browserslist": [
"Firefox ESR",

View File

@ -232,6 +232,7 @@ define([
this.actions = new api.ActionsAPI(this);
this.status = new api.StatusAPI(this);
this.styleManager = new api.StyleManagerAPI(this);
this.priority = api.PriorityAPI;
@ -241,7 +242,6 @@ 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.TelemetryTable.default());

View File

@ -31,6 +31,7 @@ define([
'./objects/ObjectAPI',
'./priority/PriorityAPI',
'./status/StatusAPI',
'./styles/StyleManagerAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry',
@ -46,6 +47,7 @@ define([
ObjectAPI,
PriorityAPI,
StatusAPI,
StyleManagerAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry,
@ -62,6 +64,7 @@ define([
ObjectAPI: ObjectAPI,
PriorityAPI: PriorityAPI.default,
StatusAPI: StatusAPI.default,
StyleManagerAPI: StyleManagerAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry,

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

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

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

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

@ -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 EventEmitter from 'EventEmitter';
export default class StyleManagerAPI extends EventEmitter {
constructor(openmct) {
super();
this._openmct = openmct;
this._styleCache = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
return this._styleCache[keyString];
}
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._styleCache[keyString] = value;
this.emit(keyString, value);
}
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._styleCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._styleCache[keyString];
}
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
this.on(key, callback);
return () => {
this.off(key, callback);
};
}
}

View File

@ -172,6 +172,7 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) {
return;
}
@ -184,8 +185,8 @@ export class TelemetryCollection extends EventEmitter {
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
beforeStartOfBounds = parsedValue <= this.lastBounds.start;
afterEndOfBounds = parsedValue >= this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
let isDuplicate = false;
@ -352,6 +353,7 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually
*/
_reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = [];
this.futureBuffer = [];

View File

@ -365,3 +365,7 @@ class TimeContext extends EventEmitter {
}
export default TimeContext;
/**
@typedef {{start: number, end: number}} Bounds
*/

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,39 @@ 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);
},
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 +205,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

@ -25,6 +25,7 @@ import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
super();
this.openmct = openmct;
this.callback = callback;
this.refreshData = this.refreshData.bind(this);
@ -152,6 +153,7 @@ export default class StyleRuleManager extends EventEmitter {
updateDomainObjectStyle() {
if (this.callback) {
this.emit('updateStyles', this.currentStyle);
this.callback(Object.assign({}, this.currentStyle));
}
}

View File

@ -27,7 +27,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
{{ label }}
</div>
</component>
</template>
@ -39,10 +39,16 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
internalDomainObject: this.domainObject
internalDomainObject: this.domainObject,
conditionalLabel: ''
};
},
computed: {
label() {
return this.conditionalLabel.length
? this.conditionalLabel
: this.internalDomainObject.label;
},
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
@ -52,13 +58,31 @@ export default {
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.unobserve = this.openmct.styleManager.observe(this.internalDomainObject.identifier, this.observeStyleManagerChanges.bind(this));
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
if (this.unobserve) {
this.openmct.styleManager.delete(this.internalDomainObject.identifier);
this.unobserve();
}
},
methods: {
observeStyleManagerChanges(styleManager) {
if (styleManager) {
this.styleManager = styleManager;
this.styleManager.on('updateStyles', this.updateConditionLabel);
} else {
this.styleManager.off('updateStyles', this.updateConditionLabel);
}
},
updateConditionLabel(styleObj = {}) {
this.conditionalLabel = styleObj.output || '';
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
}

View File

@ -222,20 +222,18 @@ 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);
if (this.mutablePromise) {
this.mutablePromise.then(() => {
@ -253,34 +251,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 +264,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 +281,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

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

@ -51,41 +51,29 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @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();
}
/**

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() {

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import ImageryView from './ImageryView';
export default function ImageryViewProvider(openmct) {
export default function ImageryViewProvider(openmct, options) {
const type = 'example.imagery';
function hasImageTelemetry(domainObject) {
@ -43,7 +43,7 @@ export default function ImageryViewProvider(openmct) {
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);
return new ImageryView(openmct, domainObject, objectPath, options);
}
};
}

View File

@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7);
position: absolute;
left: 0;
top: 0;
z-index: 1;
z-index: 2;
@include userSelectNone;
}

View File

@ -132,6 +132,10 @@
<!-- image fresh -->
<div
v-if="canTrackDuration"
:style="{
'animation-delay': imageFreshnessOptions.fadeOutDelayTime,
'animation-duration': imageFreshnessOptions.fadeOutDurationTime
}"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
@ -139,13 +143,13 @@
<!-- spacecraft position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new"
class="c-imagery__age icon-check c-imagery--new no-animation"
>POS</div>
<!-- camera position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
class="c-imagery__age icon-check c-imagery--new"
class="c-imagery__age icon-check c-imagery--new no-animation"
>CAM</div>
</div>
<div class="h-local-controls">
@ -159,10 +163,13 @@
</div>
</div>
<div
v-if="displayThumbnails"
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused },
{ 'is-small-thumbs': displayThumbnailsSmall },
{ 'hide': !displayThumbnails }
]"
>
<div
@ -175,6 +182,7 @@
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
:title="image.formattedTime"
@click="thumbnailClicked(index)"
>
<a
@ -228,6 +236,8 @@ const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
export default {
components: {
@ -235,7 +245,7 @@ export default {
ImageControls
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
props: {
focusedImageTimestamp: {
type: Number,
@ -268,6 +278,7 @@ export default {
imageContainerHeight: undefined,
sizedImageWidth: 0,
sizedImageHeight: 0,
viewHeight: 0,
lockCompass: true,
resizingWindow: false,
timeContext: undefined,
@ -286,7 +297,8 @@ export default {
imageTranslateY: 0,
pan: undefined,
animateZoom: true,
imagePanned: false
imagePanned: false,
forceShowThumbnails: false
};
},
computed: {
@ -302,6 +314,15 @@ export default {
return compassRoseSizingClasses;
},
displayThumbnails() {
return (
this.forceShowThumbnails
|| this.viewHeight >= SHOW_THUMBS_THRESHOLD_HEIGHT
);
},
displayThumbnailsSmall() {
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
},
time() {
return this.formatTime(this.focusedImage);
},
@ -310,6 +331,16 @@ export default {
},
isImageNew() {
let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
// convert css duration to IS8601 format for parsing
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
cutoff = parsedDuration + parsedDelay;
}
let age = this.numericDuration;
return age < cutoff && !this.refreshCSS;
@ -493,6 +524,8 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
},
deep: true
@ -532,7 +565,7 @@ export default {
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400, { leading: true });
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
@ -579,6 +612,9 @@ export default {
},
methods: {
calculateViewHeight() {
this.viewHeight = this.$el.clientHeight;
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@ -952,6 +988,8 @@ export default {
}
this.setSizedImageDimensions();
this.calculateViewHeight();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
@ -980,6 +1018,8 @@ export default {
this.scrollToRight('reset');
}
this.calculateViewHeight();
this.$nextTick(() => {
this.resizingWindow = false;
});

View File

@ -1,3 +1,15 @@
@use 'sass:math';
@keyframes fade-out {
from {
background-color: rgba($colorOk, 0.5);
}
to {
background-color: rgba($colorOk, 0);
color: inherit;
}
}
.c-imagery {
display: flex;
flex-direction: column;
@ -123,12 +135,20 @@
// New imagery
$bgColor: $colorOk;
color: $colorOkFg;
background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
background-color: rgba($bgColor, 0.5);
animation-name: fade-out;
animation-timing-function: ease-in;
animation-iteration-count: 1;
animation-fill-mode: forwards;
&.no-animation {
animation: none;
}
}
&__thumbs-wrapper {
display: flex; // Uses row layout
justify-content: flex-end;
&.is-autoscroll-off {
background: $colorInteriorBorder;
@ -146,17 +166,11 @@
flex: 0 1 auto;
display: flex;
flex-direction: row;
height: 135px;
height: 145px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
&__auto-scroll-resume-button {
@ -169,10 +183,12 @@
/*************************************** THUMBS */
.c-thumb {
$w: $imageThumbsD;
display: flex;
flex-direction: column;
padding: 4px;
width: $imageThumbsD;
min-width: $w;
width: $w;
&:hover {
background: $colorThumbHoverBg;
@ -194,11 +210,19 @@
}
}
.l-layout,
.c-fl {
.is-small-thumbs {
.c-imagery__thumbs-scroll-area {
// When Imagery is in a layout, hide the thumbs area
display: none;
height: 60px; // Allow room for scrollbar
}
.c-thumb {
$w: math.div($imageThumbsD, 2);
min-width: $w;
width: $w;
&__timestamp {
display: none;
}
}
}
@ -207,7 +231,7 @@
.h-local-controls--overlay-content {
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 2;
z-index: 70;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 250px;

View File

@ -23,9 +23,9 @@
import ImageryViewProvider from './ImageryViewProvider';
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
export default function () {
export default function (options) {
return function install(openmct) {
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options));
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
};
}

View File

@ -344,6 +344,8 @@ describe("The Imagery View Layouts", () => {
);
openmct.install(clearDataPlugin);
clearDataAction = openmct.actions.getAction('clear-data-action');
// force show the thumbnails
imageryView._getInstance().$children[0].forceShowThumbnails = true;
return Vue.nextTick();
});
@ -523,7 +525,10 @@ describe("The Imagery View Layouts", () => {
expect(clearDataAction).toBeDefined();
});
it('on clearData action should clear data for object is selected', (done) => {
it('on clearData action should clear data for object is selected', async (done) => {
// force show the thumbnails
imageryView._getInstance().$children[0].forceShowThumbnails = true;
await Vue.nextTick();
expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
openmct.objectViews.on('clearData', async (_domainObject) => {
await Vue.nextTick();

View File

@ -35,6 +35,8 @@
ref="searchResults"
:domain-object="domainObject"
:results="searchResults"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries"
/>
@ -140,6 +142,8 @@
:selected-page="selectedPage"
:selected-section="selectedSection"
:read-only="false"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
/>
@ -710,6 +714,8 @@ export default {
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
this.saveTransaction();
},
getPageIdFromUrl() {
return this.openmct.router.getParams().pageId;
@ -746,6 +752,39 @@ export default {
this.selectPage(pageId);
this.syncUrlWithPageAndSection();
},
activeTransaction() {
return this.openmct.objects.getActiveTransaction();
},
startTransaction() {
if (!this.openmct.editor.isEditing()) {
this.openmct.objects.startTransaction();
}
},
saveTransaction() {
const transaction = this.activeTransaction();
if (!transaction || this.openmct.editor.isEditing()) {
return;
}
return transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
},
cancelTransaction() {
if (!this.openmct.editor.isEditing()) {
const transaction = this.activeTransaction();
transaction.cancel()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}
}
};

View File

@ -55,6 +55,7 @@
class="c-ne__text c-ne__input"
tabindex="0"
contenteditable
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
@ -284,11 +285,16 @@ export default {
this.$emit('updateEntry', this.entry);
},
editingEntry() {
this.$emit('editingEntry');
},
updateEntryValue($event) {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
this.$emit('updateEntry', this.entry);
} else {
this.$emit('cancelEdit');
}
}
}

View File

@ -33,6 +33,8 @@
:read-only="true"
:selected-page="result.page"
:selected-section="result.section"
@editingEntry="editingEntry"
@cancelEdit="cancelEdit"
@changeSectionPage="changeSectionPage"
@updateEntries="updateEntries"
/>
@ -63,6 +65,12 @@ export default {
}
},
methods: {
editingEntry() {
this.$emit('editingEntry');
},
cancelEdit() {
this.$emit('cancelEdit');
},
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
},

View File

@ -20,10 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PerformancePlugin from './plugin.js';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { createOpenMct, resetApplicationState } from 'utils/testing';
describe('the plugin', () => {
let openmct;
@ -31,9 +28,8 @@ describe('the plugin', () => {
let child;
let performanceIndicator;
let countFramesPromise;
beforeEach((done) => {
beforeEach(done => {
openmct = createOpenMct();
element = document.createElement('div');
@ -42,11 +38,9 @@ describe('the plugin', () => {
openmct.install(new PerformancePlugin());
countFramesPromise = countFrames();
openmct.on('start', done);
performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => {
performanceIndicator = openmct.indicators.indicatorObjects.find(indicator => {
return indicator.text && indicator.text() === '~ fps';
});
@ -61,25 +55,21 @@ describe('the plugin', () => {
expect(performanceIndicator).toBeDefined();
});
it('correctly calculates fps', () => {
return countFramesPromise.then((frames) => {
expect(performanceIndicator.text()).toEqual(`${frames} fps`);
});
it('calculates an fps value', async () => {
await loopForABit();
// eslint-disable-next-line
expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0);
});
function countFrames() {
let startTime = performance.now();
function loopForABit() {
let frames = 0;
return new Promise((resolve) => {
requestAnimationFrame(function incrementCount() {
let now = performance.now();
if ((now - startTime) < 1000) {
frames++;
requestAnimationFrame(incrementCount);
return new Promise(resolve => {
requestAnimationFrame(function loop() {
if (++frames === 240) {
resolve();
} else {
resolve(frames);
requestAnimationFrame(loop);
}
});
});

View File

@ -37,6 +37,7 @@
v-if="seriesModels.length > 0"
:tick-width="tickWidth"
:single-series="seriesModels.length === 1"
:has-same-range-value="hasSameRangeValue"
:series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
@ -76,6 +77,9 @@
<div
ref="chartContainer"
class="gl-plot-chart-wrapper"
:class="[
{ 'alt-pressed': altPressed },
]"
>
<mct-chart
:rectangles="rectangles"
@ -230,6 +234,7 @@ export default {
},
data() {
return {
altPressed: false,
highlights: [],
lockHighlightPoint: false,
tickWidth: 0,
@ -246,7 +251,8 @@ export default {
loaded: false,
isTimeOutOfSync: false,
showLimitLineLabels: undefined,
isFrozenOnMouseDown: false
isFrozenOnMouseDown: false,
hasSameRangeValue: true
};
},
computed: {
@ -268,6 +274,8 @@ export default {
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
@ -299,9 +307,21 @@ export default {
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.destroy();
},
methods: {
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
setTimeContext() {
this.stopFollowingTimeContext();
@ -344,6 +364,7 @@ export default {
this.setDisplayRange(series, xKey);
}, this);
this.listenTo(series, 'change:yKey', () => {
this.checkSameRangeValue();
this.loadSeriesData(series);
}, this);
@ -351,10 +372,18 @@ export default {
this.loadSeriesData(series);
}, this);
this.checkSameRangeValue();
this.loadSeriesData(series);
},
checkSameRangeValue() {
this.hasSameRangeValue = this.seriesModels.every((model) => {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
},
removeSeries(plotSeries) {
this.checkSameRangeValue();
this.stopListening(plotSeries);
},
@ -470,7 +499,7 @@ export default {
},
setDisplayRange(series, xKey) {
if (this.config.series.length !== 1) {
if (this.config.series.models.length !== 1) {
return;
}
@ -643,7 +672,7 @@ export default {
this.positionOverElement = {
x: event.clientX - this.chartElementBounds.left,
y: this.chartElementBounds.height
- (event.clientY - this.chartElementBounds.top)
- (event.clientY - this.chartElementBounds.top)
};
this.positionOverPlot = {

View File

@ -30,8 +30,8 @@
class="gl-plot-tick-wrapper"
>
<div
v-for="tick in ticks"
:key="tick.value"
v-for="(tick, i) in ticks"
:key="'tick-left' + i"
class="gl-plot-tick gl-plot-x-tick-label"
:style="{
left: (100 * (tick.value - min) / interval) + '%'
@ -46,8 +46,8 @@
class="gl-plot-tick-wrapper"
>
<div
v-for="tick in ticks"
:key="tick.value"
v-for="(tick, i) in ticks"
:key="'tick-top' + i"
class="gl-plot-tick gl-plot-y-tick-label"
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
:title="tick.fullText || tick.text"
@ -59,8 +59,8 @@
<!-- grid lines follow -->
<template v-if="position === 'right'">
<div
v-for="tick in ticks"
:key="tick.value"
v-for="(tick, i) in ticks"
:key="'tick-right' + i"
class="gl-plot-hash hash-v"
:style="{
right: (100 * (max - tick.value) / interval) + '%',
@ -71,8 +71,8 @@
</template>
<template v-if="position === 'bottom'">
<div
v-for="tick in ticks"
:key="tick.value"
v-for="(tick, i) in ticks"
:key="'tick-bottom' + i"
class="gl-plot-hash hash-h"
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
>
@ -83,7 +83,7 @@
<script>
import eventHelpers from "./lib/eventHelpers";
import { ticks, getFormattedTicks } from "./tickUtils";
import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils";
import configStore from "./configuration/ConfigStore";
export default {
@ -96,6 +96,13 @@ export default {
},
required: true
},
// Make it a prop, then later we can allow user to change it via UI input
tickCount: {
type: Number,
default() {
return 6;
}
},
position: {
required: true,
type: String,
@ -112,9 +119,12 @@ export default {
mounted() {
eventHelpers.extend(this);
if (!this.axisType) {
throw new Error("axis-type prop expected");
}
this.axis = this.getAxisFromConfig();
this.tickCount = 4;
this.tickUpdate = false;
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
@ -126,15 +136,16 @@ export default {
},
methods: {
getAxisFromConfig() {
if (!this.axisType) {
return;
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
/** @type {import('./configuration/PlotConfigurationModel').default} */
let config = configStore.get(configId);
if (!config) {
throw new Error('config is missing');
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config[this.axisType];
}
return config[this.axisType];
},
/**
* Determine whether ticks should be regenerated for a given range.
@ -179,7 +190,12 @@ export default {
}, this);
}
return ticks(range.min, range.max, number);
if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
return getLogTicks(range.min, range.max, number, 4);
// return getLogTicks2(range.min, range.max, number);
} else {
return ticks(range.min, range.max, number);
}
},
updateTicksForceRegeneration() {
@ -188,6 +204,8 @@ export default {
updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange');
const logMode = this.axis.get('logMode');
if (!range) {
delete this.min;
delete this.max;
@ -210,12 +228,12 @@ export default {
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
let newTicks = this.getTicks();
this.tickRange = {
min: Math.min.apply(Math, newTicks),
max: Math.max.apply(Math, newTicks),
min: Math.min(...newTicks),
max: Math.max(...newTicks),
step: newTicks[1] - newTicks[0]
};
newTicks = getFormattedTicks(newTicks, format);
newTicks = getFormattedTicks(newTicks, format, logMode);
this.ticks = newTicks;
this.shouldCheckWidth = true;

View File

@ -29,9 +29,9 @@
>
<div
v-if="singleSeries"
v-if="canShowYAxisLabel"
class="gl-plot-label gl-plot-y-label"
:class="{'icon-gear': (yKeyOptions.length > 1)}"
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
>{{ yAxisLabel }}
</div>
@ -76,6 +76,12 @@ export default {
return true;
}
},
hasSameRangeValue: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
@ -95,6 +101,11 @@ export default {
loaded: false
};
},
computed: {
canShowYAxisLabel() {
return this.singleSeries === true || this.hasSameRangeValue === true;
}
},
mounted() {
this.yAxis = this.getYAxisFromConfig();
this.loaded = true;

View File

@ -23,6 +23,9 @@
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartAlarmLineSet {
/**
* @param {Bounds} bounds
*/
constructor(series, chart, offset, bounds) {
this.series = series;
this.chart = chart;
@ -40,6 +43,9 @@ export default class MCTChartAlarmLineSet {
}
}
/**
* @param {Bounds} bounds
*/
updateBounds(bounds) {
this.bounds = bounds;
this.getLimitPoints(this.series);
@ -106,3 +112,7 @@ export default class MCTChartAlarmLineSet {
}
}
/**
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
*/

View File

@ -23,7 +23,7 @@
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineLinear extends MCTChartSeriesElement {
addPoint(point, start, count) {
addPoint(point, start) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}

View File

@ -23,7 +23,7 @@
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
removePoint(point, index, count) {
removePoint(index) {
if (index > 0 && index / 2 < this.count) {
this.buffer[index + 1] = this.buffer[index - 1];
}
@ -45,7 +45,7 @@ export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
return 2 + ((index - 1) * 4);
}
addPoint(point, start, count) {
addPoint(point, start) {
if (start === 0 && this.count === 0) {
// First point is easy.
this.buffer[start] = point.x;

View File

@ -24,7 +24,7 @@ import MCTChartSeriesElement from './MCTChartSeriesElement';
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
export default class MCTChartPointSet extends MCTChartSeriesElement {
addPoint(point, start, count) {
addPoint(point, start) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}

View File

@ -22,6 +22,7 @@
import eventHelpers from '../lib/eventHelpers';
/** @abstract */
export default class MCTChartSeriesElement {
constructor(series, chart, offset) {
this.series = series;
@ -72,9 +73,11 @@ export default class MCTChartSeriesElement {
}
}
removePoint(point, index, count) {
// by default, do nothing.
}
/** @abstract */
removePoint(index) {}
/** @abstract */
addPoint(point, index) {}
remove(point, index, series) {
const vertexCount = this.vertexCountForPointAtIndex(index);
@ -82,11 +85,10 @@ export default class MCTChartSeriesElement {
this.removeSegments(removalPoint, vertexCount);
this.removePoint(
this.makePoint(point, series),
removalPoint,
vertexCount
);
// TODO useless makePoint call?
this.makePoint(point, series);
this.removePoint(removalPoint);
this.count -= (vertexCount / 2);
}
@ -106,11 +108,7 @@ export default class MCTChartSeriesElement {
const insertionPoint = this.startIndexForPointAtIndex(index);
this.growIfNeeded(pointsRequired);
this.makeInsertionPoint(insertionPoint, pointsRequired);
this.addPoint(
this.makePoint(point, series),
insertionPoint,
pointsRequired
);
this.addPoint(this.makePoint(point, series), insertionPoint);
this.count += (pointsRequired / 2);
}
@ -155,3 +153,18 @@ export default class MCTChartSeriesElement {
}
}
/** @typedef {any} TODO */
/** @typedef {import('../configuration/PlotSeries').default} PlotSeries */
/**
@typedef {{
x: (x: number) => number
y: (y: number) => number
xVal: (point: Point, pSeries: PlotSeries) => number
yVal: (point: Point, pSeries: PlotSeries) => number
xKey: TODO
yKey: TODO
}} Offset
*/

View File

@ -21,11 +21,17 @@
*****************************************************************************/
import Model from './Model';
/**
* @template {object} T
* @template {object} O
* @extends {Model<T, O>}
*/
export default class Collection extends Model {
/** @type {Constructor} */
modelClass = Model;
initialize(options) {
super.initialize(options);
this.modelClass = Model;
if (options.models) {
this.models = options.models.map(this.modelFn, this);
} else {
@ -107,3 +113,7 @@ export default class Collection extends Model {
this.stopListening();
}
}
/** @typedef {any} TODO */
/** @typedef {new (...args: any[]) => object} Constructor */

View File

@ -19,32 +19,47 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
function ConfigStore() {
this.store = {};
}
class ConfigStore {
/** @type {Record<string, Destroyable>} */
store = {};
ConfigStore.prototype.deleteStore = function (id) {
if (this.store[id]) {
if (this.store[id].destroy) {
this.store[id].destroy();
/**
@param {string} id
*/
deleteStore(id) {
const obj = this.store[id];
if (obj) {
if (obj.destroy) {
obj.destroy();
}
delete this.store[id];
}
delete this.store[id];
}
};
ConfigStore.prototype.deleteAll = function () {
Object.keys(this.store).forEach(id => this.deleteStore(id));
};
deleteAll() {
Object.keys(this.store).forEach(id => this.deleteStore(id));
}
ConfigStore.prototype.add = function (id, config) {
this.store[id] = config;
};
/**
@param {string} id
@param {any} config
*/
add(id, config) {
this.store[id] = config;
}
ConfigStore.prototype.get = function (id) {
return this.store[id];
};
/**
@param {string} id
*/
get(id) {
return this.store[id];
}
}
const STORE = new ConfigStore();
export default STORE;
/** @typedef {{destroy?(): void}} Destroyable */

View File

@ -42,7 +42,10 @@ export default class LegendModel extends Model {
}
}
defaults(options) {
/**
* @override
*/
defaultModel(options) {
return {
position: 'top',
expandByDefault: false,

View File

@ -20,11 +20,18 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import EventEmitter from 'eventemitter3';
import eventHelpers from "../lib/eventHelpers";
import _ from 'lodash';
/**
* @template {object} T
* @template {object} O
*/
export default class Model extends EventEmitter {
/**
* @param {ModelOptions<T, O>} options
*/
constructor(options) {
super();
@ -35,10 +42,14 @@ export default class Model extends EventEmitter {
options = {};
}
// FIXME: this.id is defined as a method further below, but here it is
// assigned a possibly-undefined value. Is this code unused?
this.id = options.id;
/** @type {ModelType<T>} */
this.model = options.model;
this.collection = options.collection;
const defaults = this.defaults(options);
const defaults = this.defaultModel(options);
if (!this.model) {
this.model = options.model = defaults;
} else {
@ -46,14 +57,24 @@ export default class Model extends EventEmitter {
}
this.initialize(options);
/** @type {keyof ModelType<T> } */
this.idAttr = 'id';
}
defaults(options) {
/**
* @param {ModelOptions<T, O>} options
* @returns {ModelType<T>}
*/
defaultModel(options) {
return {};
}
initialize(model) {
/**
* @abstract
* @param {ModelOptions<T, O>} options
*/
initialize(options) {
}
@ -69,14 +90,29 @@ export default class Model extends EventEmitter {
return this.get(this.idAttr);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @returns {ModelType<T>[K]}
*/
get(attribute) {
return this.model[attribute];
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @returns boolean
*/
has(attribute) {
return _.has(this.model, attribute);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @param {ModelType<T>[K]} value
*/
set(attribute, value) {
const oldValue = this.model[attribute];
this.model[attribute] = value;
@ -84,6 +120,10 @@ export default class Model extends EventEmitter {
this.emit('change:' + attribute, value, oldValue, this);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
*/
unset(attribute) {
const oldValue = this.model[attribute];
delete this.model[attribute];
@ -91,3 +131,26 @@ export default class Model extends EventEmitter {
this.emit('change:' + attribute, undefined, oldValue, this);
}
}
/** @typedef {any} TODO */
/** @typedef {TODO} OpenMCT */
/**
@template {object} T
@typedef {{
id?: string
} & T} ModelType
*/
/**
@template {object} T
@template {object} O
@typedef {{
model?: ModelType<T>
models?: T[]
openmct: OpenMCT
id?: string
[k: string]: unknown
} & O} ModelOptions
*/

View File

@ -26,20 +26,30 @@ import SeriesCollection from "./SeriesCollection";
import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The indiidual parts of the plot configuration model
* handle setting defaults and updating in response to various changes.
*
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
*/
export default class PlotConfigurationModel extends Model {
/**
* Initializes all sub models and then passes references to submodels
* to those that need it.
*
* @override
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
*/
initialize(options) {
this.openmct = options.openmct;
// This is a type assertion for TypeScript, this error is never thrown in practice.
if (!options.model) {
throw new Error('Not a collection model.');
}
this.xAxis = new XAxisModel({
model: options.model.xAxis,
plot: this,
@ -76,6 +86,8 @@ export default class PlotConfigurationModel extends Model {
}
/**
* Retrieve the persisted series config for a given identifier.
* @param {import('./PlotSeries').Identifier} identifier
* @returns {import('./PlotSeries').PlotSeriesModelType=}
*/
getPersistedSeriesConfig(identifier) {
const domainObject = this.get('domainObject');
@ -123,15 +135,49 @@ export default class PlotConfigurationModel extends Model {
/**
* Return defaults, which are extracted from the passed in domain
* object.
* @override
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
*/
defaults(options) {
defaultModel(options) {
return {
series: [],
domainObject: options.domainObject,
xAxis: {
},
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
xAxis: {},
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
};
}
}
/** @typedef {any} TODO */
/** @typedef {import('./PlotSeries').default} PlotSeries */
/**
@typedef {{
configuration: {
series: import('./PlotSeries').PlotSeriesModelType[]
yAxis: import('./YAxisModel').YAxisModelType
},
}} SomeDomainObject_NeedsName
*/
/**
@typedef {{
xAxis: import('./XAxisModel').XAxisModelType
yAxis: import('./YAxisModel').YAxisModelType
legend: TODO
series: PlotSeries[]
domainObject: SomeDomainObject_NeedsName
}} PlotConfigModelType
*/
/** @typedef {TODO} SomeOtherDomainObject */
/**
TODO: Is SomeOtherDomainObject the same domain object as with SomeDomainObject_NeedsName?
@typedef {{
plot: import('./PlotConfigurationModel').default
domainObject: SomeOtherDomainObject
}} PlotConfigModelOptions
*/

View File

@ -23,6 +23,7 @@ import _ from 'lodash';
import Model from "./Model";
import { MARKER_SHAPES } from '../draw/MarkerShapes';
import configStore from "../configuration/ConfigStore";
import { symlog } from '../mathUtils';
/**
* Plot series handle interpreting telemetry metadata for a single telemetry
@ -59,11 +60,21 @@ import configStore from "../configuration/ConfigStore";
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
* telemetry point.
* `formats`: the Open MCT format map for this telemetry point.
*
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
*/
export default class PlotSeries extends Model {
logMode = false;
/**
@param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
*/
constructor(options) {
super(options);
this.logMode = options.collection.plot.model.yAxis.logMode;
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
this.persistedConfig = options.persistedConfig;
@ -76,8 +87,10 @@ export default class PlotSeries extends Model {
/**
* Set defaults for telemetry series.
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
* @override
*/
defaults(options) {
defaultModel(options) {
this.metadata = options
.openmct
.telemetry
@ -109,13 +122,21 @@ export default class PlotSeries extends Model {
/**
* Remove real-time subscription when destroyed.
* @override
*/
onDestroy(model) {
destroy() {
super.destroy();
if (this.unsubscribe) {
this.unsubscribe();
}
}
/**
* Set defaults for telemetry series.
* @override
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
*/
initialize(options) {
this.openmct = options.openmct;
this.domainObject = options.domainObject;
@ -136,9 +157,11 @@ export default class PlotSeries extends Model {
});
this.openmct.time.on('bounds', this.updateLimits);
this.on('destroy', this.onDestroy, this);
}
/**
* @param {Bounds} bounds
*/
updateLimits(bounds) {
this.emit('limitBounds', bounds);
}
@ -188,7 +211,7 @@ export default class PlotSeries extends Model {
return this.openmct
.telemetry
.request(this.domainObject, options)
.then(function (points) {
.then((points) => {
const data = this.getSeriesData();
const newPoints = _(data)
.concat(points)
@ -196,7 +219,7 @@ export default class PlotSeries extends Model {
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value();
this.reset(newPoints);
}.bind(this))
})
.catch((error) => {
console.warn('Error fetching data', error);
});
@ -211,6 +234,7 @@ export default class PlotSeries extends Model {
this.getXVal = format.parse.bind(format);
}
}
/**
* Update y formatter on change, default to stepAfter interpolation if
* y range is an enumeration.
@ -232,8 +256,13 @@ export default class PlotSeries extends Model {
this.evaluate = function (datum) {
return this.limitEvaluator.evaluate(datum, valueMetadata);
}.bind(this);
this.set('unit', valueMetadata.unit);
const format = this.formats[newKey];
this.getYVal = format.parse.bind(format);
this.getYVal = (value) => {
const y = format.parse(value);
return this.logMode ? symlog(y, 10) : y;
};
}
formatX(point) {
@ -501,8 +530,56 @@ export default class PlotSeries extends Model {
/**
* Update the series data with the given value.
* This return type definition is totally wrong, only covers sinwave generator. It needs to be generic.
* @return-example {Array<{
cos: number
sin: number
mctLimitState: {
cssClass: string
high: number
low: {sin: number, cos: number}
name: string
}
utc: number
wavelength: number
yesterday: number
}>}
*/
getSeriesData() {
return configStore.get(this.dataStoreId) || [];
}
}
/** @typedef {any} TODO */
/** @typedef {{key: string, namespace: string}} Identifier */
/**
@typedef {{
identifier: Identifier
name: string
unit: string
xKey: string
yKey: string
markers: boolean
markerShape: keyof typeof MARKER_SHAPES
markerSize: number
alarmMarkers: boolean
limitLines: boolean
interpolate: boolean
stats: TODO
}} PlotSeriesModelType
*/
/**
@typedef {{
model: PlotSeriesModelType
collection: import('./SeriesCollection').default
persistedConfig: PlotSeriesModelType
filters: TODO
}} PlotSeriesModelOptions
*/
/**
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
*/

View File

@ -26,8 +26,14 @@ import Collection from "./Collection";
import Color from "@/ui/color/Color";
import ColorPalette from "@/ui/color/ColorPalette";
/**
* @extends {Collection<SeriesCollectionModelType, SeriesCollectionOptions>}
*/
export default class SeriesCollection extends Collection {
/**
@override
@param {import('./Model').ModelOptions<SeriesCollectionModelType, SeriesCollectionOptions>} options
*/
initialize(options) {
super.initialize(options);
this.modelClass = PlotSeries;
@ -83,11 +89,15 @@ export default class SeriesCollection extends Collection {
// Clone to prevent accidental mutation by ref.
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
if (!seriesConfig) {
throw "not possible";
}
this.add(new PlotSeries({
model: seriesConfig,
domainObject: domainObject,
collection: this,
openmct: this.openmct,
collection: this,
persistedConfig: this.plot
.getPersistedSeriesConfig(domainObject.identifier),
filters: filters
@ -163,3 +173,13 @@ export default class SeriesCollection extends Collection {
})[0];
}
}
/**
@typedef {PlotSeries} SeriesCollectionModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} SeriesCollectionOptions
*/

View File

@ -19,25 +19,41 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Model from "./Model";
import Model from './Model';
/**
* TODO: doc strings.
*/
* @extends {Model<XAxisModelType, XAxisModelOptions>}
*/
export default class XAxisModel extends Model {
// Despite providing template types to the Model class, we still need to
// re-define the type of the following initialize() method's options arg. Tracking
// issue for this: https://github.com/microsoft/TypeScript/issues/32082
// When they fix it, we can remove the `@param` we have here.
/**
* @override
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
*/
initialize(options) {
this.plot = options.plot;
// This is a type assertion for TypeScript, this error is not thrown in practice.
if (!options.model) {
throw new Error('Not a collection model.');
}
this.set('label', options.model.name || '');
this.on('change:range', function (newValue, oldValue, model) {
if (!model.get('frozen')) {
model.set('displayRange', newValue);
this.on('change:range', (newValue) => {
if (!this.get('frozen')) {
this.set('displayRange', newValue);
}
});
this.on('change:frozen', ((frozen, oldValue, model) => {
this.on('change:frozen', (frozen) => {
if (!frozen) {
model.set('range', this.get('range'));
this.set('range', this.get('range'));
}
}));
});
if (this.get('range')) {
this.set('range', this.get('range'));
@ -45,6 +61,10 @@ export default class XAxisModel extends Model {
this.listenTo(this, 'change:key', this.changeKey, this);
}
/**
* @param {string} newKey
*/
changeKey(newKey) {
const series = this.plot.series.first();
if (series) {
@ -68,12 +88,17 @@ export default class XAxisModel extends Model {
plotSeries.reset();
});
}
defaults(options) {
/**
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
* @override
*/
defaultModel(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
return {
/** @type {XAxisModelType} */
const defaultModel = {
name: timeSystem.name,
key: timeSystem.key,
format: format.format.bind(format),
@ -83,5 +108,42 @@ export default class XAxisModel extends Model {
},
frozen: false
};
return defaultModel;
}
}
/** @typedef {any} TODO */
/** @typedef {TODO} OpenMCT */
/**
@typedef {{
min: number
max: number
}} NumberRange
*/
/**
@typedef {import("./Model").ModelType<{
range?: NumberRange
displayRange: NumberRange
frozen: boolean
label: string
format: (n: number) => string
values: Array<TODO>
}>} AxisModelType
*/
/**
@typedef {AxisModelType & {
name: string
key: string
}} XAxisModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} XAxisModelOptions
*/

View File

@ -19,58 +19,62 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import { antisymlog, symlog } from '../mathUtils';
import Model from './Model';
/**
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the x Axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
*/
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
* @extends {Model<YAxisModelType, YAxisModelOptions>}
*/
export default class YAxisModel extends Model {
/**
* @override
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
*/
initialize(options) {
this.plot = options.plot;
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
this.listenTo(this, 'change:logMode', this.onLogModeChange, this);
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
this.updateDisplayRange(this.get('range'));
}
/**
* @param {import('./SeriesCollection').default} seriesCollection
*/
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', (series => {
this.listenTo(this.seriesCollection, 'add', series => {
this.trackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
this.listenTo(this.seriesCollection, 'remove', (series => {
}, this);
this.listenTo(this.seriesCollection, 'remove', series => {
this.untrackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
}, this);
this.seriesCollection.forEach(this.trackSeries, this);
this.updateFromSeries(this.seriesCollection);
}
updateDisplayRange(range) {
if (!this.get('autoscale')) {
this.set('displayRange', range);
}
}
toggleFreeze(frozen) {
if (!frozen) {
this.toggleAutoscale(this.get('autoscale'));
@ -132,12 +136,15 @@ export default class YAxisModel extends Model {
}
resetStats() {
this.unset('stats');
this.seriesCollection.forEach(function (series) {
this.seriesCollection.forEach(series => {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
}, this);
});
}
/**
* @param {import('./PlotSeries').default} series
*/
trackSeries(series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (!seriesStats) {
@ -155,20 +162,104 @@ export default class YAxisModel extends Model {
this.resetStats();
this.updateFromSeries(this.seriesCollection);
}
/**
* This is called in order to map the user-provided `range` to the
* `displayRange` that we actually use for plot display.
*
* @param {import('./XAxisModel').NumberRange} range
*/
updateDisplayRange(range) {
if (this.get('autoscale')) {
return;
}
const _range = { ...range };
if (this.get('logMode')) {
_range.min = symlog(range.min, 10);
_range.max = symlog(range.max, 10);
}
this.set('displayRange', _range);
}
/**
* @param {boolean} autoscale
*/
toggleAutoscale(autoscale) {
if (autoscale && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
return;
}
const range = this.get('range');
if (range) {
// If we already have a user-defined range, make sure it maps to the
// range we'll actually use for the ticks.
const _range = { ...range };
if (this.get('logMode')) {
_range.min = symlog(range.min, 10);
_range.max = symlog(range.max, 10);
}
this.set('displayRange', _range);
} else {
this.set('displayRange', this.get('range'));
// Otherwise use the last known displayRange as the initial
// values for the user-defined range, so that we don't end up
// with any error from an undefined user range.
const _range = this.get('displayRange');
if (!_range) {
return;
}
if (this.get('logMode')) {
_range.min = antisymlog(_range.min, 10);
_range.max = antisymlog(_range.max, 10);
}
this.set('range', _range);
}
}
/** @param {boolean} logMode */
onLogModeChange(logMode) {
const range = this.get('displayRange');
if (logMode) {
range.min = symlog(range.min, 10);
range.max = symlog(range.max, 10);
} else {
range.min = antisymlog(range.min, 10);
range.max = antisymlog(range.max, 10);
}
this.set('displayRange', range);
this.resetSeries();
}
resetSeries() {
this.plot.series.forEach((plotSeries) => {
plotSeries.logMode = this.get('logMode');
plotSeries.reset(plotSeries.getSeriesData());
});
// Update the series collection labels and formatting
this.updateFromSeries(this.seriesCollection);
}
/**
* Update yAxis format, values, and label from known series.
*/
updateFromSeries(series) {
* Update yAxis format, values, and label from known series.
* @param {import('./SeriesCollection').default} seriesCollection
*/
updateFromSeries(seriesCollection) {
const plotModel = this.plot.get('domainObject');
const label = _.get(plotModel, 'configuration.yAxis.label');
const sampleSeries = series.first();
const label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first();
if (!sampleSeries) {
if (!label) {
this.unset('label');
@ -180,22 +271,28 @@ export default class YAxisModel extends Model {
const yKey = sampleSeries.get('yKey');
const yMetadata = sampleSeries.metadata.value(yKey);
const yFormat = sampleSeries.formats[yKey];
this.set('format', yFormat.format.bind(yFormat));
if (this.get('logMode')) {
this.set('format', (n) => yFormat.format(antisymlog(n, 10)));
} else {
this.set('format', (n) => yFormat.format(n));
}
this.set('values', yMetadata.values);
if (!label) {
const labelName = series.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
const labelName = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
if (a === b) {
return a;
}
return '';
}, undefined);
return '';
}, undefined);
if (labelName) {
this.set('label', labelName);
@ -203,19 +300,19 @@ export default class YAxisModel extends Model {
return;
}
const labelUnits = series.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
const labelUnits = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
if (a === b) {
return a;
}
return '';
}, undefined);
return '';
}, undefined);
if (labelUnits) {
this.set('label', labelUnits);
@ -224,11 +321,39 @@ export default class YAxisModel extends Model {
}
}
}
defaults(options) {
/**
* @override
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
* @returns {Partial<YAxisModelType>}
*/
defaultModel(options) {
return {
frozen: false,
autoscale: true,
logMode: options.model?.logMode ?? false,
autoscalePadding: 0.1
// 'range' is not specified here, it is undefined at first. When the
// user turns off autoscale, the current 'displayRange' is used for
// the initial value of 'range'.
};
}
}
/** @typedef {any} TODO */
/**
@typedef {import('./XAxisModel').AxisModelType & {
autoscale: boolean
logMode: boolean
autoscalePadding: number
stats?: import('./XAxisModel').NumberRange
values: Array<TODO>
}} YAxisModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} YAxisModelOptions
*/

View File

@ -110,6 +110,7 @@ DrawWebGL.prototype.onContextLost = function (event) {
this.emit('error');
this.isContextLost = true;
this.destroy();
// TODO re-initialize and re-draw on context restored
};
DrawWebGL.prototype.initContext = function () {

View File

@ -48,11 +48,19 @@
<li class="grid-row">
<div
class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Autoscale</div>
title="Enable log mode."
>Log mode</div>
<div class="grid-cell value">
{{ autoscale ? "Enabled: " : "Disabled" }}
{{ autoscale ? autoscalePadding : "" }}
{{ logMode ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Auto scale</div>
<div class="grid-cell value">
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
</div>
</li>
<li
@ -142,6 +150,7 @@ export default {
config: {},
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
@ -172,6 +181,7 @@ export default {
initConfiguration() {
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.logMode = this.config.yAxis.get('logMode');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {

View File

@ -14,9 +14,22 @@
@change="updateForm('label')"
></div>
</li>
</ul>
<ul class="l-inspector-part">
<h2>Y Axis Scaling</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="Enable log mode."
>
Log mode
</div>
<div class="grid-cell value">
<!-- eslint-disable-next-line vue/html-self-closing -->
<input
v-model="logMode"
type="checkbox"
@change="updateForm('logMode')"
/>
</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
@ -52,10 +65,10 @@
class="l-inspector-part"
>
<div
v-show="!autoscale && validation.range"
v-show="!autoscale && validationErrors.range"
class="grid-span-all form-error"
>
{{ validation.range }}
{{ validationErrors.range }}
</div>
<li class="grid-row force-border">
<div
@ -88,7 +101,7 @@
</template>
<script>
import { objectPath, validate, coerce } from "./formUtil";
import { objectPath } from "./formUtil";
import _ from "lodash";
export default {
@ -105,10 +118,11 @@ export default {
return {
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
validation: {}
validationErrors: {}
};
},
mounted() {
@ -117,38 +131,35 @@ export default {
},
methods: {
initialize: function () {
this.fields = [
{
modelProp: 'label',
this.fields = {
label: {
objectPath: 'configuration.yAxis.label'
},
{
modelProp: 'autoscale',
autoscale: {
coerce: Boolean,
objectPath: 'configuration.yAxis.autoscale'
},
{
modelProp: 'autoscalePadding',
autoscalePadding: {
coerce: Number,
objectPath: 'configuration.yAxis.autoscalePadding'
},
{
modelProp: 'range',
logMode: {
coerce: Boolean,
objectPath: 'configuration.yAxis.logMode'
},
range: {
objectPath: 'configuration.yAxis.range',
coerce: function coerceRange(range) {
if (!range) {
return {
min: 0,
max: 0
};
}
const newRange = {
min: -1,
max: 1
};
const newRange = {};
if (typeof range.min !== 'undefined' && range.min !== null) {
if (range && typeof range.min !== 'undefined' && range.min !== null) {
newRange.min = Number(range.min);
}
if (typeof range.max !== 'undefined' && range.max !== null) {
if (range && typeof range.max !== 'undefined' && range.max !== null) {
newRange.max = Number(range.max);
}
@ -178,28 +189,18 @@ export default {
if (Number(range.min) > Number(range.max)) {
return 'Minimum must be less than Maximum.';
}
if (model.get('autoscale')) {
return false;
}
return true;
}
}
];
};
},
initFormValues() {
this.label = this.yAxis.get('label');
this.autoscale = this.yAxis.get('autoscale');
this.logMode = this.yAxis.get('logMode');
this.autoscalePadding = this.yAxis.get('autoscalePadding');
const range = this.yAxis.get('range');
if (!range) {
this.rangeMin = undefined;
this.rangeMax = undefined;
} else {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
this.rangeMin = range?.min;
this.rangeMax = range?.max;
},
updateForm(formKey) {
let newVal;
@ -212,26 +213,27 @@ export default {
newVal = this[formKey];
}
const oldVal = this.yAxis.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.yAxis, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
let oldVal = this.yAxis.get(formKey);
const formField = this.fields[formKey];
const validationError = formField.validate?.(newVal, this.yAxis);
this.validationErrors[formKey] = validationError;
if (validationError) {
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.yAxis.set(formKey, coerce(newVal, formField.coerce));
newVal = formField.coerce?.(newVal) ?? newVal;
oldVal = formField.coerce?.(oldVal) ?? oldVal;
const path = objectPath(formField.objectPath);
if (!_.isEqual(newVal, oldVal)) {
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
this.yAxis.set(formKey, newVal);
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
coerce(newVal, formField.coerce)
newVal
);
}
}

View File

@ -15,15 +15,5 @@ export function validate(value, model, validateFunc) {
}
export function objectPath(path) {
if (path) {
if (typeof path !== "function") {
const staticObjectPath = path;
return function (object, model) {
return staticObjectPath;
};
}
return path;
}
return path && typeof path !== 'function' ? () => path : path;
}

View File

@ -90,3 +90,10 @@ const helperFunctions = {
};
export default helperFunctions;
/**
@typedef {{
listenTo: (object: any, event: any, callback: any, context: any) => void
stopListening: (object: any, event: any, callback: any, context: any) => void
}} EventHelpers
*/

View File

@ -0,0 +1,44 @@
/** The natural number `e`. */
export const e = Math.exp(1);
/**
Returns the logarithm of a number, using the given base or the natural number
`e` as base if not specified.
@param {number} n
@param {number=} base log base, defaults to e
*/
export function log(n, base = e) {
if (base === e) {
return Math.log(n);
}
return Math.log(n) / Math.log(base);
}
/**
Returns the inverse of the logarithm of a number, using the given base or the
natural number `e` as base if not specified.
@param {number} n
@param {number=} base log base, defaults to e
*/
export function antilog(n, base = e) {
return Math.pow(base, n);
}
/**
A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
@param {number} n
@param {number=} base log base, defaults to e
*/
export function symlog(n, base = e) {
return Math.sign(n) * log(Math.abs(n) + 1, base);
}
/**
An inverse symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
@param {number} n
@param {number=} base log base, defaults to e
*/
export function antisymlog(n, base = e) {
return Math.sign(n) * (antilog(Math.abs(n), base) - 1);
}

View File

@ -389,7 +389,7 @@ describe("the plugin", function () {
expect(xAxisElement.length).toBe(1);
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(5);
expect(ticks.length).toBe(9);
done();
});
@ -571,6 +571,34 @@ describe("the plugin", function () {
range: 2
}
}]
},
configuration: {
objectStyles: {
staticStyle: {
style: {
backgroundColor: 'rgb(0, 200, 0)',
color: '',
border: ''
}
},
conditionSetIdentifier: {
namespace: '',
key: 'testConditionSetId'
},
selectedConditionId: 'conditionId1',
defaultConditionId: 'conditionId1',
styles: [
{
conditionId: 'conditionId1',
style: {
backgroundColor: 'rgb(0, 155, 0)',
color: '',
output: '',
border: ''
}
}
]
}
}
};
@ -666,7 +694,7 @@ describe("the plugin", function () {
Vue.nextTick(() => {
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(5);
expect(ticks.length).toBe(9);
done();
});
@ -815,6 +843,20 @@ describe("the plugin", function () {
});
});
it("shows styles for telemetry objects if available", (done) => {
Vue.nextTick(() => {
let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
let hasStyles = 0;
conditionalStylesContainer.forEach(el => {
if (el.style.backgroundColor !== '') {
hasStyles++;
}
});
expect(hasStyles).toBe(1);
done();
});
});
describe('limits', () => {
it('lines are not displayed by default', () => {
@ -1044,7 +1086,9 @@ describe("the plugin", function () {
expandControl.dispatchEvent(clickEvent);
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
expect(yAxisProperties.length).toEqual(3);
// TODO better test
expect(yAxisProperties.length).toEqual(2);
});
it('renders color palette options', () => {

View File

@ -26,8 +26,10 @@
import MctPlot from '../MctPlot.vue';
import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
export default {
mixins: [conditionalStylesMixin],
inject: ['openmct', 'domainObject', 'path'],
props: {
object: {

View File

@ -0,0 +1,137 @@
/*****************************************************************************
* 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 StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
export default {
inject: ['openmct', 'domainObject', 'path'],
data() {
return {
objectStyle: undefined
};
},
mounted() {
this.objectStyles = this.getObjectStyleForItem(this.object.configuration);
this.initObjectStyles();
},
beforeDestroy() {
if (this.stopListeningStyles) {
this.stopListeningStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
}
},
methods: {
getObjectStyleForItem(config) {
if (config && config.objectStyles) {
return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined;
} else {
return undefined;
}
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager(this.objectStyles, this.openmct, this.updateStyle.bind(this), true);
} else {
this.styleRuleManager.updateObjectStyleConfig(this.objectStyles);
}
if (this.stopListeningStyles) {
this.stopListeningStyles();
}
this.stopListeningStyles = this.openmct.objects.observe(this.object, 'configuration.objectStyles', (newObjectStyle) => {
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
if (this.object && this.object.configuration && this.object.configuration.fontStyle) {
const { fontSize, font } = this.object.configuration.fontStyle;
this.setFontSize(fontSize);
this.setFont(font);
}
this.stopListeningFontStyles = this.openmct.objects.observe(this.object, 'configuration.fontStyle', (newFontStyle) => {
this.setFontSize(newFontStyle.fontSize);
this.setFont(newFontStyle.font);
});
},
getStyleReceiver() {
let styleReceiver;
if (this.$el !== undefined) {
styleReceiver = this.$el.querySelector('.js-style-receiver')
|| this.$el.querySelector(':first-child');
if (styleReceiver === null) {
styleReceiver = undefined;
}
}
return styleReceiver;
},
setFontSize(newSize) {
let elemToStyle = this.getStyleReceiver();
if (elemToStyle !== undefined) {
elemToStyle.dataset.fontSize = newSize;
}
},
setFont(newFont) {
let elemToStyle = this.getStyleReceiver();
if (elemToStyle !== undefined) {
elemToStyle.dataset.font = newFont;
}
},
updateStyle(styleObj) {
let elemToStyle = this.getStyleReceiver();
if (!styleObj || elemToStyle === undefined) {
return;
}
let keys = Object.keys(styleObj);
keys.forEach(key => {
if (elemToStyle) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (elemToStyle.style[key]) {
elemToStyle.style[key] = '';
}
} else {
if (!styleObj.isStyleInvisible && elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)) {
elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
} else if (styleObj.isStyleInvisible && !elemToStyle.classList.contains(styleObj.isStyleInvisible)) {
elemToStyle.classList.add(styleObj.isStyleInvisible);
}
elemToStyle.style[key] = styleObj[key];
}
}
});
}
}
};

View File

@ -1,3 +1,5 @@
import { antisymlog, symlog } from "./mathUtils";
const e10 = Math.sqrt(50);
const e5 = Math.sqrt(10);
const e2 = Math.sqrt(2);
@ -40,6 +42,47 @@ function getPrecision(step) {
return precision;
}
export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) {
// log()'ed values
const mainLogTicks = ticks(start, stop, mainTickCount);
// original values
const mainTicks = mainLogTicks.map(n => antisymlog(n, 10));
const result = [];
let i = 0;
for (const logTick of mainLogTicks) {
result.push(logTick);
if (i === mainLogTicks.length - 1) {
break;
}
const tick = mainTicks[i];
const nextTick = mainTicks[i + 1];
const rangeBetweenMainTicks = nextTick - tick;
const secondaryLogTicks = ticks(
tick + rangeBetweenMainTicks / (secondaryTickCount + 1),
nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1),
secondaryTickCount - 2
)
.map(n => symlog(n, 10));
result.push(...secondaryLogTicks);
i++;
}
return result;
}
export function getLogTicks2(start, stop, count = 8) {
return ticks(antisymlog(start, 10), antisymlog(stop, 10), count)
.map(n => symlog(n, 10));
}
/**
* Linear tick generation from d3-array.
*/
@ -88,12 +131,17 @@ export function commonSuffix(a, b) {
return a.slice(a.length - breakpoint);
}
export function getFormattedTicks(newTicks, format) {
export function getFormattedTicks(newTicks, format, formatFloat) {
newTicks = newTicks
.map(function (tickValue) {
let formattedValue = format(tickValue);
if (formatFloat === true && typeof formattedValue === 'number' && !Number.isInteger(formattedValue)) {
formattedValue = parseFloat(formattedValue).toFixed(2);
}
return {
value: tickValue,
text: format(tickValue)
text: formattedValue
};
});

View File

@ -76,7 +76,9 @@ define([
'./timer/plugin',
'./userIndicator/plugin',
'../../example/exampleUser/plugin',
'./localStorage/plugin'
'./localStorage/plugin',
'./gauge/GaugePlugin',
'./timelist/plugin'
], function (
_,
UTCTimeSystem,
@ -133,7 +135,9 @@ define([
Timer,
UserIndicator,
ExampleUser,
LocalStorage
LocalStorage,
GaugePlugin,
TimeList
) {
const plugins = {};
@ -210,6 +214,8 @@ define([
plugins.DeviceClassifier = DeviceClassifier.default;
plugins.UserIndicator = UserIndicator.default;
plugins.LocalStorage = LocalStorage.default;
plugins.Gauge = GaugePlugin.default;
plugins.Timelist = TimeList.default;
return plugins;
});

View File

@ -9,7 +9,11 @@ export default class TelemetryTableView {
this.objectPath = objectPath;
this.component = undefined;
this.table = new TelemetryTable(domainObject, openmct);
Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct),
enumerable: false,
configurable: false
});
}
getViewContext() {

View File

@ -29,10 +29,6 @@ define(
_,
EventEmitter
) {
const LESS_THAN = -1;
const EQUAL = 0;
const GREATER_THAN = 1;
/**
* @constructor
*/
@ -80,10 +76,7 @@ define(
this.rows = [];
}
for (let row of rowsToAdd) {
let index = this.sortedIndex(this.rows, row);
this.rows.splice(index, 0, row);
}
this.sortAndMergeRows(rowsToAdd);
// we emit filter no matter what to trigger
// an update of visible rows
@ -92,58 +85,85 @@ define(
}
}
sortedLastIndex(rows, testRow) {
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
}
sortAndMergeRows(rows) {
const sortedRowsToAdd = this.sortCollection(rows);
/**
* Finds the correct insertion point for the given row.
* Leverages lodash's `sortedIndex` function which implements a binary search.
* @private
*/
sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
if (this.rows.length === 0) {
return 0;
this.rows = sortedRowsToAdd;
return;
}
const testRowValue = this.getValueForSortColumn(testRow);
const firstValue = this.getValueForSortColumn(this.rows[0]);
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
const firstIncomingRow = sortedRowsToAdd[0];
const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1];
const firstExistingRow = this.rows[0];
const lastExistingRow = this.rows[this.rows.length - 1];
if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow)
=== lastIncomingRow
) {
this.rows = [...sortedRowsToAdd, ...this.rows];
} else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow)
=== lastExistingRow
) {
this.rows = [...this.rows, ...sortedRowsToAdd];
} else {
this.mergeSortedRows(sortedRowsToAdd);
}
}
sortCollection(rows) {
const sortedRows = _.orderBy(
rows,
row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction
);
return sortedRows;
}
mergeSortedRows(rows) {
const mergedRows = [];
let i = 0;
let j = 0;
while (i < this.rows.length && j < rows.length) {
const existingRow = this.rows[i];
const incomingRow = rows[j];
if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {
mergedRows.push(existingRow);
i++;
} else {
mergedRows.push(incomingRow);
j++;
}
}
// tail of existing rows is all that is left to merge
if (i < this.rows.length) {
for (i; i < this.rows.length; i++) {
mergedRows.push(this.rows[i]);
}
}
// tail of incoming rows is all that is left to merge
if (j < rows.length) {
for (j; j < rows.length; j++) {
mergedRows.push(rows[j]);
}
}
this.rows = mergedRows;
}
firstRowInSortOrder(row1, row2) {
const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2);
if (this.sortOptions.direction === 'asc') {
if (testRowValue > lastValue) {
return this.rows.length;
} else if (testRowValue === lastValue) {
// Maintain stable sort
return this.rows.length;
} else if (testRowValue <= firstValue) {
return 0;
} else {
return lodashFunction(rows, testRow, (thisRow) => {
return this.getValueForSortColumn(thisRow);
});
}
return val1 <= val2 ? row1 : row2;
} else {
if (testRowValue >= firstValue) {
return 0;
} else if (testRowValue < lastValue) {
return this.rows.length;
} else if (testRowValue === lastValue) {
// Maintain stable sort
return this.rows.length;
} else {
// Use a custom comparison function to support descending sort.
return lodashFunction(rows, testRow, (thisRow) => {
const thisRowValue = this.getValueForSortColumn(thisRow);
if (testRowValue === thisRowValue) {
return EQUAL;
} else if (testRowValue < thisRowValue) {
return LESS_THAN;
} else {
return GREATER_THAN;
}
});
}
return val1 >= val2 ? row1 : row2;
}
}
@ -205,8 +225,9 @@ define(
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
performance.mark('table:row:sort:start');
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
performance.mark('table:row:sort:stop');
this.emit('sort');
}

View File

@ -613,6 +613,7 @@ export default {
this.calculateScrollbarWidth();
},
sortBy(columnKey) {
performance.mark('table:sort');
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
@ -669,6 +670,7 @@ export default {
this.setHeight();
},
rowsAdded(rows) {
performance.mark('row:added');
this.setHeight();
let sizingRow;
@ -690,6 +692,7 @@ export default {
this.updateVisibleRows();
},
rowsRemoved(rows) {
performance.mark('row:removed');
this.setHeight();
this.updateVisibleRows();
},

View File

@ -0,0 +1,394 @@
<!--
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
ref="timelistHolder"
class="c-timelist"
>
<list-view
:items="planActivities"
:header-items="headerItems"
:default-sort="defaultSort"
class="sticky"
/>
</div>
</template>
<script>
import {getValidatedPlan} from "../plan/util";
import ListView from '../../ui/components/List/ListView.vue';
import {getPreciseDuration} from "../../utils/duration";
import ticker from 'utils/clock/Ticker';
import {SORT_ORDER_OPTIONS} from "./constants";
import moment from "moment";
import uuid from "uuid";
const SCROLL_TIMEOUT = 10000;
const ROW_HEIGHT = 30;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
const headerItems = [
{
defaultDirection: true,
isSortable: true,
property: 'start',
name: 'Start Time',
format: function (value, object) {
return `${moment(value).format(TIME_FORMAT)}Z`;
}
}, {
defaultDirection: true,
isSortable: true,
property: 'end',
name: 'End Time',
format: function (value, object) {
return `${moment(value).format(TIME_FORMAT)}Z`;
}
}, {
defaultDirection: false,
property: 'duration',
name: 'Time To/From',
format: function (value) {
let result;
if (value < 0) {
result = `-${getPreciseDuration(Math.abs(value))}`;
} else if (value > 0) {
result = `+${getPreciseDuration(value)}`;
} else {
result = 'Now';
}
return result;
}
}, {
defaultDirection: true,
property: 'name',
name: 'Activity'
}
];
const defaultSort = {
property: 'start',
defaultDirection: true
};
export default {
components: {
ListView
},
inject: ['openmct', 'domainObject', 'path'],
data() {
return {
viewBounds: undefined,
height: 0,
planActivities: [],
headerItems: headerItems,
defaultSort: defaultSort
};
},
mounted() {
this.isEditing = this.openmct.editor.isEditing();
this.timestamp = Date.now();
this.getPlanDataAndSetConfig(this.domainObject);
this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.getPlanDataAndSetConfig);
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.domainObject.identifier);
this.unlistenTicker = ticker.listen(this.clearPreviousActivities);
this.openmct.editor.on('isEditing', this.setEditState);
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
if (this.unlistenConfig) {
this.unlistenConfig();
}
if (this.unlistenTicker) {
this.unlistenTicker();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.openmct.editor.off('isEditing', this.setEditState);
this.$el.parentElement.removeEventListener('scroll', this.deferAutoScroll, true);
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
},
methods: {
getPlanDataAndSetConfig(mutatedObject) {
this.getPlanData(mutatedObject);
this.setViewFromConfig(mutatedObject.configuration);
},
setViewFromConfig(configuration) {
if (this.isEditing) {
this.filterValue = configuration.filter;
this.hideAll = false;
this.showAll = true;
this.listActivities();
} else {
this.filterValue = configuration.filter;
this.setSort();
this.setViewBounds();
this.listActivities();
}
},
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
setViewBounds() {
const pastEventsIndex = this.domainObject.configuration.pastEventsIndex;
const currentEventsIndex = this.domainObject.configuration.currentEventsIndex;
const futureEventsIndex = this.domainObject.configuration.futureEventsIndex;
const pastEventsDuration = this.domainObject.configuration.pastEventsDuration;
const pastEventsDurationIndex = this.domainObject.configuration.pastEventsDurationIndex;
const futureEventsDuration = this.domainObject.configuration.futureEventsDuration;
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
//show all events
this.showAll = false;
this.viewBounds = undefined;
this.hideAll = true;
return;
}
this.hideAll = false;
if (pastEventsIndex === 1 && futureEventsIndex === 1 && currentEventsIndex === 1) {
//show all events
this.showAll = true;
this.viewBounds = undefined;
return;
}
this.showAll = false;
this.viewBounds = {};
this.noCurrent = currentEventsIndex === 0;
if (pastEventsIndex !== 1) {
const pastDurationInMS = this.getDurationInMilliSeconds(pastEventsDuration, pastEventsDurationIndex);
this.viewBounds.pastEnd = (timestamp) => {
if (pastEventsIndex === 2) {
return timestamp - pastDurationInMS;
} else if (pastEventsIndex === 0) {
return timestamp + 1;
}
};
}
if (futureEventsIndex !== 1) {
const futureDurationInMS = this.getDurationInMilliSeconds(futureEventsDuration, futureEventsDurationIndex);
this.viewBounds.futureStart = (timestamp) => {
if (futureEventsIndex === 2) {
return timestamp + futureDurationInMS;
} else if (futureEventsIndex === 0) {
return 0;
}
};
}
},
getDurationInMilliSeconds(duration, durationIndex) {
if (duration > 0) {
if (durationIndex === 0) {
return duration * 1000;
} else if (durationIndex === 1) {
return duration * 60 * 1000;
} else if (durationIndex === 2) {
return duration * 60 * 60 * 1000;
}
}
},
listActivities() {
let groups = Object.keys(this.planData);
let activities = [];
groups.forEach((key) => {
activities = activities.concat(this.planData[key]);
});
activities = activities.filter(this.filterActivities);
activities = this.applyStyles(activities);
this.setScrollTop();
// sort by start time
this.planActivities = activities.sort(this.sortByStartTime);
},
clearPreviousActivities(time) {
if (time instanceof Date) {
this.timestamp = time.getTime();
} else {
this.timestamp = time;
}
this.listActivities();
},
filterActivities(activity, index) {
const hasFilterMatch = this.filterByName(activity.name);
if (hasFilterMatch === false || this.hideAll === true) {
return false;
}
if (this.showAll === true) {
return true;
}
//current event or future start event or past end event
const isCurrent = (this.noCurrent === false && this.timestamp >= activity.start && this.timestamp <= activity.end);
const isPast = (this.timestamp > activity.end && (this.viewBounds.pastEnd === undefined || activity.end >= this.viewBounds.pastEnd(this.timestamp)));
const isFuture = (this.timestamp < activity.start && (this.viewBounds.futureStart === undefined || activity.start <= this.viewBounds.futureStart(this.timestamp)));
return isCurrent || isPast || isFuture;
},
filterByName(name) {
const filters = this.filterValue.split(',');
return filters.some((search => {
const normalized = search.trim().toLowerCase();
const regex = new RegExp(normalized);
return regex.test(name.toLowerCase());
}));
},
applyStyles(activities) {
let firstCurrentActivityIndex = -1;
let currentActivitiesCount = 0;
const styledActivities = activities.map((activity, index) => {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = '--is-current';
if (firstCurrentActivityIndex < 0) {
firstCurrentActivityIndex = index;
}
currentActivitiesCount = currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) {
activity.cssClass = '--is-future';
} else {
activity.cssClass = '--is-past';
}
if (!activity.key) {
activity.key = uuid();
}
activity.duration = activity.start - this.timestamp;
return activity;
});
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
this.currentActivitiesCount = currentActivitiesCount;
return styledActivities;
},
canAutoScroll() {
//this distinguishes between programmatic vs user-triggered scroll events
this.autoScrolled = (this.dontAutoScroll !== true);
return this.autoScrolled;
},
resetScroll() {
if (this.canAutoScroll() === false) {
return;
}
this.firstCurrentActivityIndex = -1;
this.currentActivitiesCount = 0;
this.$el.parentElement.scrollTo({top: 0});
this.autoScrolled = false;
},
setScrollTop() {
//scroll to somewhere mid-way of the current activities
if (this.firstCurrentActivityIndex > -1) {
if (this.canAutoScroll() === false) {
return;
}
const scrollOffset = this.currentActivitiesCount > 0 ? Math.floor(this.currentActivitiesCount / 2) : 0;
this.$el.parentElement.scrollTo({
top: ROW_HEIGHT * (this.firstCurrentActivityIndex + scrollOffset),
behavior: "smooth"
});
this.autoScrolled = false;
} else {
this.resetScroll();
}
},
deferAutoScroll() {
//if this is not a user-triggered event, don't defer auto scrolling
if (this.autoScrolled) {
this.autoScrolled = false;
return;
}
this.dontAutoScroll = true;
const self = this;
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
this.clearAutoScrollDisabledTimer = setTimeout(() => {
self.dontAutoScroll = false;
self.setScrollTop();
}, SCROLL_TIMEOUT);
},
setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
const property = sortOrder.property;
const direction = sortOrder.direction.toLowerCase() === 'asc';
this.defaultSort = {
property,
defaultDirection: direction
};
},
sortByStartTime(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
return numA - numB;
},
setStatus(status) {
this.status = status;
},
setEditState(isEditing) {
this.isEditing = isEditing;
this.setViewFromConfig(this.domainObject.configuration);
}
}
};
</script>

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 Timelist from './Timelist.vue';
import { TIMELIST_TYPE } from './constants';
import Vue from 'vue';
export default function TimelistViewProvider(openmct) {
return {
key: 'timelist.view',
name: 'Time List',
cssClass: 'icon-timelist',
canView(domainObject) {
return domainObject.type === TIMELIST_TYPE;
},
canEdit(domainObject) {
return domainObject.type === TIMELIST_TYPE;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Timelist
},
provide: {
openmct,
domainObject,
path: objectPath
},
template: '<timelist></timelist>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -0,0 +1,24 @@
export const SORT_ORDER_OPTIONS = [
{
label: 'Start ascending',
property: 'start',
direction: 'ASC'
},
{
label: 'Start descending',
property: 'start',
direction: 'DESC'
},
{
label: 'End ascending',
property: 'end',
direction: 'ASC'
},
{
label: 'End descending',
property: 'end',
direction: 'DESC'
}
];
export const TIMELIST_TYPE = 'timelist';

View File

@ -0,0 +1,124 @@
<template>
<li class="c-inspect-properties__row">
<div
class="c-inspect-properties__label"
title="Options for future events."
>{{ label }}</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
>
<select
v-model="index"
@change="updateForm('index')"
>
<option
v-for="(activityOption, activityKey) in activitiesOptions"
:key="activityKey"
:value="activityKey"
>{{ activityOption }}</option>
</select>
<input
v-if="index === 2"
v-model="duration"
class="c-input c-input--sm"
type="text"
@change="updateForm('duration')"
>
<select
v-if="index === 2"
v-model="durationIndex"
@change="updateForm('durationIndex')"
>
<option
v-for="(durationOption, durationKey) in durationOptions"
:key="durationKey"
:value="durationKey"
>{{ durationOption }}</option>
</select>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ activitiesOptions[index] }} <span v-if="index > 1">{{ duration }} {{ durationOptions[durationIndex] }}</span>
</div>
</li>
</template>
<script>
const ACTIVITIES_OPTIONS = [
'Don\'t show',
'Show all',
'Show starts within',
'Show after end for'
];
const DURATION_OPTIONS = [
'seconds',
'minutes',
'hours'
];
export default {
inject: ['openmct', 'domainObject'],
props: {
label: {
type: String,
required: true
},
prefix: {
type: String,
required: true
}
},
data() {
return {
index: this.domainObject.configuration[`${this.prefix}Index`],
durationIndex: this.domainObject.configuration[`${this.prefix}DurationIndex`],
duration: this.domainObject.configuration[`${this.prefix}Duration`],
activitiesOptions: ACTIVITIES_OPTIONS,
durationOptions: DURATION_OPTIONS,
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
if (this.prefix === 'futureEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 3);
} else if (this.prefix === 'pastEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.filter((item, index) => index !== 2);
} else if (this.prefix === 'currentEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 2);
}
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
updateForm(property) {
if (!this.isValid()) {
return;
}
const capitalized = property.charAt(0).toUpperCase() + property.substr(1);
this.$emit('updated', {
property: `${this.prefix}${capitalized}`,
value: this[property]
});
},
isValid() {
return this.index < 2 || (this.durationIndex >= 0 && this.duration > 0);
},
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@ -0,0 +1,91 @@
<template>
<li class="c-inspect-properties__row">
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
>Filter this view by comma-separated keywords.</div>
<div
class="c-inspect-properties__label"
title="Filter by keyword."
>Filters</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
:class="{'form-error': hasError}"
>
<textarea
v-model="filterValue"
class="c-input--flex"
type="text"
@keydown.enter.exact.stop="forceBlur($event)"
@keyup="updateForm($event, 'filter')"
></textarea>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ filterValue }}
</div>
</li>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),
filterValue: this.domainObject.configuration.filter,
hasError: false
};
},
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;
if (!this.isEditing && this.hasError) {
this.filterValue = this.domainObject.configuration.filter;
this.hasError = false;
}
},
forceBlur(event) {
event.target.blur();
},
updateForm(event, property) {
if (!this.isValid()) {
this.hasError = true;
return;
}
this.hasError = false;
this.$emit('updated', {
property,
value: this.filterValue.replace(/,(\s)*$/, '')
});
},
isValid() {
// Test for any word character, any whitespace character or comma
if (this.filterValue === '') {
return true;
}
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
return regex.test(this.filterValue);
}
}
};
</script>

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* 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 TimelistPropertiesView from "./TimelistPropertiesView.vue";
import { TIMELIST_TYPE } from '../constants';
import Vue from 'vue';
export default function TimeListInspectorViewProvider(openmct) {
return {
key: 'timelist-inspector',
name: 'Timelist Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context && context.item
&& context.item.type === TIMELIST_TYPE;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
TimelistPropertiesView: TimelistPropertiesView
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<timelist-properties-view></timelist-properties-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

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>
<div class="c-timelist-properties">
<div class="c-inspect-properties">
<ul class="c-inspect-properties__section">
<div
class="c-inspect-properties_header"
title="'Timeframe options'"
>Timeframe</div>
<li class="c-inspect-properties__row">
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
>These settings are not previewed and will be applied after editing is completed.</div>
<div
class="c-inspect-properties__label"
title="Sort order of the timelist."
>Sort Order</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
>
<select
v-model="sortOrderIndex"
@change="updateSortOrder()"
>
<option
v-for="(sortOrderOption, index) in sortOrderOptions"
:key="index"
:value="index"
>{{ sortOrderOption.label }}</option>
</select>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ sortOrderOptions[sortOrderIndex].label }}
</div>
</li>
<event-properties
v-for="type in eventTypes"
:key="type.prefix"
:label="type.label"
:prefix="type.prefix"
@updated="eventPropertiesUpdated"
/>
</ul>
</div>
<div class="c-inspect-properties">
<ul class="c-inspect-properties__section">
<div
class="c-inspect-properties_header"
title="'Filters'"
>Filtering</div>
<filtering @updated="eventPropertiesUpdated" />
</ul>
</div>
</div>
</template>
<script>
import EventProperties from './EventProperties.vue';
import { SORT_ORDER_OPTIONS } from '../constants';
import Filtering from './Filtering.vue';
const EVENT_TYPES = [
{
label: 'Future Events',
prefix: 'futureEvents'
},
{
label: 'Current Events',
prefix: 'currentEvents'
},
{
label: 'Past Events',
prefix: 'pastEvents'
}
];
export default {
components: {
Filtering,
EventProperties
},
inject: ['openmct', 'domainObject'],
data() {
return {
sortOrderIndex: this.domainObject.configuration.sortOrderIndex,
sortOrderOptions: SORT_ORDER_OPTIONS,
eventTypes: EVENT_TYPES,
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;
},
updateSortOrder() {
this.updateProperty('sortOrderIndex', this.sortOrderIndex);
},
updateProperty(key, value) {
this.openmct.objects.mutate(this.domainObject, `configuration.${key}`, value);
},
eventPropertiesUpdated(data) {
const key = data.property;
const value = data.value;
this.updateProperty(key, value);
}
}
};
</script>

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* 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 TimelistViewProvider from './TimelistViewProvider';
import { TIMELIST_TYPE } from './constants';
import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider";
export default function () {
return function install(openmct) {
openmct.types.addType(TIMELIST_TYPE, {
name: 'Time List',
key: TIMELIST_TYPE,
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
cssClass: 'icon-timelist',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
futureEventsIndex: 0,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 0,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
};
}
});
openmct.objectViews.addProvider(new TimelistViewProvider(openmct));
openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));
};
}

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