Compare commits

..

61 Commits

Author SHA1 Message Date
fd4dcc8513 Add check for destroy method on store.
Change failing test to use new data getter.
2021-08-10 09:55:22 -07:00
9ebd18318b Remve console log 2021-08-10 09:55:22 -07:00
4a89b81f4f Logging for beforeunload events 2021-08-10 09:55:22 -07:00
98e1abd7b1 Don't draw points if the count is 0 2021-08-10 09:55:22 -07:00
56c25762ac Fix resize handler for plots 2021-08-10 09:55:22 -07:00
5c8e726b87 Fix data store id 2021-08-10 09:55:22 -07:00
d80f4a1f7d Revert commented out code 2021-08-10 09:55:22 -07:00
3fe4c7a954 Revert eslint changes 2021-08-10 09:55:22 -07:00
676ef60128 Revert eslint changes 2021-08-10 09:55:22 -07:00
5a90d28450 Separate plot series data from the configuration (like it should be!) 2021-08-10 09:55:22 -07:00
2bb6822e6b Draft 2021-08-10 09:55:22 -07:00
383b4c0d8d Fix no mutating props violation for Browsebar and StyleEditor 2021-08-10 09:55:22 -07:00
404ab720ad Enable no mutating props vue lint configuration. Fix error for plots 2021-08-10 09:55:22 -07:00
259ab53060 Refactor clock object and clock indicator to remove AngularJS dependency (#4094)
* To enable clock indicator, pass in the following configuration { enableClockIndicator: true }.
2021-08-09 14:29:45 -07:00
1db7ac55b4 [Imagery] Click on image to get a large view #3582 (#4085)
fixed issue where large imagery view opens only once.
2021-08-04 15:44:50 -07:00
82b3383834 Set the yKey value on the series when it's changed (#4083) 2021-08-04 13:56:37 -07:00
ac240d524c Add check for stop observing before calling it (#4080) 2021-08-04 10:40:16 -07:00
1b034f6125 remove can edit from hyperlink (#4076) 2021-08-03 16:01:29 -07:00
b329ed6ed5 Couch object provider performance improvement using SharedWorker (#3993)
* Use the window SharedWorker instead of the WorkerService
* Use relative asset path for Shared Workers
* Remove beforeunload listener on destroy
2021-07-30 15:23:02 -07:00
9b7a0d7e4c Reimplement hyperlink vue (#4062)
* added vue hyperlink plugin
* remove angular code. update target attribute
* Polishing on form styles
- Remove `display: flex` from `.l-shell__main-container` and
`.c-so-view__object-view` CSS - IMPORTANT: NEEDS REGRESSION TESTING!
- Improvements to `.c-hyperlink` CSS;
- Markup cleanups and simplification;
- Remove duped CSS in object-frame.scss, probably result of prior bad
past merge;
* Fixes for object-frame and preview.scss
* Refinement to make hyperlink button have same display behavior as
Condition Widget;
* refactor layout template. update tests
* remove legacy hyperlink
* Updating firefox launcher

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
2021-07-30 13:49:31 -07:00
5c15e53abb Mct4039 (#4057)
Re-implements ImageExportService as ES6 class instead of Angular managed service.

Co-authored-by: John Hill <jchill2.spam@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 22:05:18 -07:00
f58b3881f2 Not everything fits into the two types of issue (#4064)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 17:17:51 -07:00
071a13b219 [Imagery] Click on image to get a large view (#3770)
* [Imagery] Click on image to get a large view #3582
* Created new viewLargeAction.
* Changes in view registry to add parent element property inside view object.
* Separate class for views and added missing changes for LadTableSet.
* Renamed callBack to onItemClicked.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 09:19:07 -07:00
ca66898e51 [Plugin] Remote Clock based off Telemetry (#3998)
* Remote-clock plugin
* Added a default clock class that can be extended by other classes
* Updated local clock to use the parent default clock class
* added a period check to make sure its been a certain amount of time before emitting
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-28 16:46:08 -07:00
94c7b2343a Add Enhancement request template (#4063)
* Add Enhancement request template

* Link to github discussions

* Update config.yml
2021-07-27 15:02:30 -07:00
c397c336ab [Telemetry Tables] Observe for changes in table configuration (#4053) 2021-07-27 14:08:44 -07:00
eea23f2caf Fix typo in template (#4059) 2021-07-27 11:05:02 -07:00
6665641c02 Update issue templates (#4058)
Add a bug report directly to the project
2021-07-27 10:38:24 -07:00
c3ebf52dd2 Prepare for sprint 1.7.6 (#4052) 2021-07-26 14:04:26 -07:00
f8f2e7da9b Remove deprecated timeline bundle (#4048) 2021-07-23 13:51:32 -07:00
240f58b2d0 [Telemetry API] wrap limits function return in promise if needed (#4044) 2021-07-20 07:25:43 -07:00
7d3baee7b5 URL Params to hide tree and inspector (#3951)
* Add checks and hide panes accordingly, toggle hide params when toggling panes, add params on change event
* add tests

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-19 10:01:05 -07:00
1f5cb7ca42 Prevent default on click events for conductor delta buttons (#4022)
* Prevent default on click events for conductor delta buttons
2021-07-15 14:46:53 -07:00
4a7ebe326c Plot view policy fix (#3995)
* deny plot view for non-numeric telemetry

* revert plot type for backwards compatibility
2021-07-14 18:12:26 -07:00
10da314a4a Add resize event and disable pointer events on iframes on trigger of resize (#4016) 2021-07-14 17:32:45 -07:00
b3ceccd7fb temporarily skip test on chrome (#4021) 2021-07-14 15:58:44 -07:00
1bde4c9a0c Update all unit test packages, set node engine rules, new circleci workflow, pin to stable (#3957)
* Update all unit test packages, set node engine rules, new circleci workflow

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-14 09:26:38 -07:00
4b85360446 1.7.4 stable master (#4015) 2021-07-13 13:59:37 -07:00
41b860a547 Fixes for Imagery Snapshotting #3963 (#4001)
* refactored compass structure and code.
* resize image wrapper
* center image properly
* Refactor imagery compass rose as SVG
* Suppress prev/next image arrows from Snapshot

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2021-07-09 18:03:11 -07:00
254b3db966 added mock values to compass for example imagery. (#3999) 2021-07-09 15:13:54 -07:00
cbb3f32d1e 1.7.4 into master (#3985)
catching any errors from a user canceling from dialog (#3968)
Fix navigation errors (#3970)
Only add listeners to observables on creation
Do not double destroy mutable objects
Disallow pause and play in time strip view for plots. (#3972)
Update version

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-29 09:40:30 -07:00
e3bf72e77f New time conductor popups should be closed by hitting ESC key (#3978) 2021-06-29 09:03:58 -07:00
0b63b782cf Image thumbs autoscroll resumption functionality (#3892)
* add logs for testing

* check autoScroll value

* Add auto scroll button at thumbnails

* add auto scroll button functionality

* turn on auto scroll whenever scroll bar is not at the right end

* check if scroll right when the page load

* update scroll to right condition

* remove resetScroll method, refactor scrollToRight and some cleaning

* check initial status

* remove logs

* Styling for imagery thumbs 'autoscroll resume' button

- Refined look and feel of the approach;
- CSS classes now assigned at the wrapper level;
- Markup changed to allow wrapping of scroll area and button;
- TODO: prevent a drag resize of the main view area from forcing
autoscroll to pause;

* Add tests for auto scroll

* add resize observer for thumb wrapper

* reset scroll bar position when window is resized

* Revert "upgrade to webpack5 (#3871)" (#3907) (#3908)

This reverts commit e1e0eeac56.

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

* Check for getlimits api (#3910)

* add debounce and resizing observer to thumbwrapper

* handling resizing window and auto scroll

* 1.clean up comments and logs 2.make variable names more descriptive 3.add scroll to right for play button

* fix eslint formate issue

* update class name in test

* fix eslint format error

* remove a couple variables that were created but not used.

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
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>
2021-06-29 07:52:27 -07:00
da39fd0c70 Overlay fixes and improvements (#3901)
* Overlay-related fixes
- Prevent navigation when a folder's grid or list view is displayed in
an overlay;

* Overlay-related fixes
- Get rid of theme-based special overlay coloring that was making the
overlay hard to use;
- Add margin to `l-overlay-large` to make it clearer that user is in an
overlay when interacting with that view;
- Refinements to colors and layout in About screens;
2021-06-28 14:35:35 -07:00
96dd581a67 Timestrip plan Inspection (#3863)
* Stub in static HTML for Timestrip Activity Inspection
- Added static markup with placeholder values and display logic;
- Refined approach for Links;
* Refactor duration formatting
* Display activity name when it's available
* Don't use indices for keys
* Don't show properties with no labels
2021-06-28 10:04:54 -07:00
2a1e322230 adding link to discussions in readme to encourage users to showcase their use of Open MCT (#3933)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-22 09:34:36 -07:00
300b98bd54 Disallow pan, zoom and pause/play controls in time strip view (#3936)
* Disallow pan and zoom when in time strip view

* Disable plot controls in time strip view
2021-06-22 09:25:12 -07:00
c946609d13 Added LGTM code quality badge (#3960)
We score an A, we should flaunt it! (We should also aim for A+).
2021-06-22 09:10:08 -07:00
7ca559fbe4 [Styles] add unit tests (#3557)
* unit tests for inspector styles feature
* add mock capability for local storage

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-22 06:50:49 -07:00
71392915c1 Mct3834 (#3962)
* remove redundant code

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
2021-06-21 17:23:42 -07:00
2889e88a97 Styling for plot limits (#3917)
* Styling for plot limits and colors
* Updates to limit provider and css
* Change limits related CSS "*--upper" and "*--lower" to "*--upr" and
"*--lwr" for better parity with legacy naming;
* Refactor limit class to util
* Use new classes for sine wave generator for red-low and yellow-high
* Added modifier classes for right and below-aligned labels;
* Prevent label overlap of limits as much as possible
* Add border colors to limit labels for better visual ties to their lines
* Add documentation for limit level specification API change

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-21 16:22:28 -07:00
d56d176aac Issue #3834 Reimplement New Tab action in vanilla JS (#3876)
* [Reimplement] create new action plugin for issue #3834

Co-authored-by mariuszr mariusz.rosinski@gmail.com
Co-authored-by: Henry Hsu <henryhsu@henrys-air.lan>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-21 16:11:31 -07:00
925518c83f [Notebooks] Don't save images on the object (#3792)
* Create and store image data into new domain object of type 'notebookSnapshotImage'
* Reduced thumbnail size to 30px
* Image migration script for old notebooks.
* Saves thumbnail image on notebook instead of new object.
2021-06-21 15:42:33 -07:00
fa5aceb7b3 Bind method to 'this' so that its listeners are correctly unbound on destroy (#3948)
* Bind method to 'this' so that its listener can work correctly
* Bind this for toggling subscriptions as well
2021-06-21 10:44:59 -07:00
6755ef4641 Support for remote mutation of Notebooks with Couch DB (#3887)
* Update notebook automatically when modified by another user
* Don't persist selected and default page and section IDs on notebook object
* Fixing object synchronization bugs
* Adding unit tests
* Synchronize notebooks AND plans
* Removed observeEnabled flag

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-21 10:25:17 -07:00
333e8b5583 [Testing] Resolve all promises (#3829)
* all promises in test specs should be returned

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-18 16:06:15 -07:00
9d8a8b36d2 Move duplicate fixes (#3947)
* Changed text of form labels;
* Corrected case of "Location" in Duplicate action;
* changed from objects.mutate to objects.save for duplicate action name change
* handling cancel of move

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
2021-06-17 14:04:47 -07:00
b484a4a959 Initial LighthouseCI Commit (#3906)
* Initial LighthouseCI Commit

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-16 11:37:37 -07:00
64e7c62d98 [Style] check if there is an element to style before applying style (#3950)
* check if there is an element to style

* add mmgis (external plugin) type to style exclusion list

* revert b919cf9 to be fixed properly

* reduce code
2021-06-15 16:03:23 -07:00
6483fe2402 Prepare master for Sprint 1.7.4 (#3925) 2021-06-07 11:37:49 -07:00
a123889d6a Pre release for Sprint 1.7.3 (#3924)
* Revert "upgrade to webpack5 (#3871)" (#3907) (#3908)
* [Navigation Tree] Fix composition on closed folders and scrolling for items NOT in tree (#3920)
* Update package.json version and version documentation to include tags for npmjs

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-06-07 11:25:58 -07:00
225 changed files with 6646 additions and 3341 deletions

View File

@ -1,36 +1,69 @@
version: 2 version: 2.1
jobs: executors:
build: linux:
docker: docker:
- image: circleci/node:13-browsers - image: cimg/base:stable
environment: orbs:
CHROME_BIN: "/usr/bin/google-chrome" node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
jobs:
test:
parameters:
node-version:
type: string
browser:
type: string
always-pass:
type: boolean
executor: linux
steps: steps:
- checkout - checkout
- run:
name: Update npm
command: 'sudo npm install -g npm@latest'
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package.json" }} key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- run: - node/install:
name: Installing dependencies (npm install) node-version: << parameters.node-version >>
command: npm install - node/install-packages:
override-ci-command: npm install
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-chrome:
replace-existing: false
- save_cache: - save_cache:
key: dependency-cache-{{ checksum "package.json" }} key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
paths: paths:
- ~/.npm
- ~/.cache
- node_modules - node_modules
- run: - run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
name: npm run test:coverage - store_test_results:
command: npm run test:coverage path: dist/reports/tests/
- run:
name: npm run lint
command: npm run lint
- store_artifacts: - store_artifacts:
path: dist path: dist/reports/
prefix: dist
workflows: workflows:
version: 2 matrix-tests:
test:
jobs: jobs:
- build - test:
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true

View File

@ -1,11 +1,12 @@
<!--- This is for filing bugs. If you have a general question, please -->
<!--- visit https://github.com/nasa/openmct/discussions -->
--- ---
name: Bug Report name: Bug report
about: File a Bug ! about: File a Bug !
title: ''
labels: type:bug
assignees: ''
--- ---
<!--- Focus on user impact in the title. Use the Summary Field to --> <!--- Focus on user impact in the title. Use the Summary Field to -->
<!--- describe the problem technically. --> <!--- describe the problem technically. -->
@ -35,7 +36,7 @@ about: File a Bug !
#### Environment #### Environment
* Open MCT Version: <!--- date of build, version, or SHA --> * Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? --> * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS: * OS:
* Browser: * Browser:

View File

@ -1 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/nasa/openmct/discussions
about: Got a question?

View File

@ -0,0 +1,20 @@
---
name: Enhancement request
about: Suggest an enhancement or new improvement for this project
title: ''
labels: type:enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

20
.github/workflows/lighthouse.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
- run: lhci autorun

3
.gitignore vendored
View File

@ -40,4 +40,7 @@ npm-debug.log
# karma reports # karma reports
report.*.json report.*.json
# Lighthouse reports
.lighthouseci
package-lock.json package-lock.json

14
API.md
View File

@ -595,9 +595,17 @@ section.
#### Limit Evaluators **draft** #### Limit Evaluators **draft**
Limit evaluators allow a telemetry integrator to define how limits should be Limit evaluators allow a telemetry integrator to define which limits exist for a
applied to telemetry from a given domain object. For an example of a limit telemetry endpoint and how limits should be applied to telemetry from a given domain object.
evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
A limit evaluator can implement the `evalute` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels.
Limit levels can be mapped to one of 5 colors for visualization:
`purple`, `red`, `orange`, `yellow` and `cyan`.
For an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
### Telemetry Consumer APIs **draft** ### Telemetry Consumer APIs **draft**

View File

@ -1,9 +1,11 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) # Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/) Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action ## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/). Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).

View File

@ -132,6 +132,7 @@ numbers by the following process:
4. Test the package before publishing by doing `npm publish --dry-run` 4. Test the package before publishing by doing `npm publish --dry-run`
if necessary. if necessary.
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`) 5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
NOTE: Use the `--tag unstable` flag to the npm publishj if this is a prerelease.
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`) 6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
5. Update snapshot status in `package.json` 5. Update snapshot status in `package.json`
1. Create a new branch off the `master` branch. 1. Create a new branch off the `master` branch.

View File

@ -26,14 +26,26 @@ define([
) { ) {
var RED = { var PURPLE = {
sin: 2.2,
cos: 2.2
},
RED = {
sin: 0.9, sin: 0.9,
cos: 0.9 cos: 0.9
}, },
ORANGE = {
sin: 0.7,
cos: 0.7
},
YELLOW = { YELLOW = {
sin: 0.5, sin: 0.5,
cos: 0.5 cos: 0.5
}, },
CYAN = {
sin: 0.45,
cos: 0.45
},
LIMITS = { LIMITS = {
rh: { rh: {
cssClass: "is-limit--upr is-limit--red", cssClass: "is-limit--upr is-limit--red",
@ -94,32 +106,66 @@ define([
}; };
SinewaveLimitProvider.prototype.getLimits = function (domainObject) { SinewaveLimitProvider.prototype.getLimits = function (domainObject) {
return { return {
limits: function () { limits: function () {
return { return Promise.resolve({
WATCH: {
low: {
color: "cyan",
sin: -CYAN.sin,
cos: -CYAN.cos
},
high: {
color: "cyan",
...CYAN
}
},
WARNING: { WARNING: {
low: { low: {
cssClass: "is-limit--lwr is-limit--yellow", color: "yellow",
sin: -YELLOW.sin, sin: -YELLOW.sin,
cos: -YELLOW.cos cos: -YELLOW.cos
}, },
high: { high: {
cssClass: "is-limit--upr is-limit--yellow", color: "yellow",
...YELLOW ...YELLOW
} }
}, },
DISTRESS: { DISTRESS: {
low: { low: {
cssClass: "is-limit--lwr is-limit--red", color: "orange",
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: "orange",
...ORANGE
}
},
CRITICAL: {
low: {
color: "red",
sin: -RED.sin, sin: -RED.sin,
cos: -RED.cos cos: -RED.cos
}, },
high: { high: {
cssClass: "is-limit--upr is-limit--red", color: "red",
...RED ...RED
} }
},
SEVERE: {
low: {
color: "purple",
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: "purple",
...PURPLE
} }
}; }
});
} }
}; };
}; };

View File

@ -49,6 +49,10 @@ define([
]; ];
const IMAGE_DELAY = 20000; const IMAGE_DELAY = 20000;
function getCompassValues(min, max) {
return min + Math.random() * (max - min);
}
function pointForTimestamp(timestamp, name) { function pointForTimestamp(timestamp, name) {
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]; const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
const urlItems = url.split('/'); const urlItems = url.split('/');
@ -59,6 +63,9 @@ define([
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url, url,
sunOrientation: getCompassValues(0, 360),
cameraPan: getCompassValues(0, 360),
heading: getCompassValues(0, 360),
imageDownloadName imageDownloadName
}; };
} }

View File

@ -1,4 +1,4 @@
import Vue from 'Vue'; import Vue from 'vue';
import HelloWorld from './HelloWorld.vue'; import HelloWorld from './HelloWorld.vue';
function SimpleVuePlugin() { function SimpleVuePlugin() {

View File

@ -88,6 +88,7 @@
openmct.install(openmct.plugins.ExampleImagery()); openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout()); openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline()); openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({ openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel" type: "telemetry.panel"
@ -194,6 +195,7 @@
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true} {indicator: true}
)); ));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.start(); openmct.start();
</script> </script>
</html> </html>

View File

@ -23,9 +23,9 @@
/*global module,process*/ /*global module,process*/
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless']; const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true'; const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html']; const reporters = ['progress', 'html', 'junit'];
if (coverageEnabled) { if (coverageEnabled) {
reporters.push('coverage-istanbul'); reporters.push('coverage-istanbul');
@ -59,7 +59,8 @@ module.exports = (config) => {
browsers: browsers, browsers: browsers,
client: { client: {
jasmine: { jasmine: {
random: false random: false,
timeoutInterval: 30000
} }
}, },
customLaunchers: { customLaunchers: {
@ -67,6 +68,10 @@ module.exports = (config) => {
base: 'Chrome', base: 'Chrome',
flags: ['--remote-debugging-port=9222'], flags: ['--remote-debugging-port=9222'],
debug: true debug: true
},
FirefoxESR: {
base: 'FirefoxHeadless',
name: 'FirefoxESR'
} }
}, },
colors: true, colors: true,
@ -78,12 +83,21 @@ module.exports = (config) => {
preserveDescribeNesting: true, preserveDescribeNesting: true,
foldAll: false foldAll: false
}, },
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true }, junitReporter: {
outputDir: "dist/reports/tests",
outputFile: "test-results.xml",
useBrowserName: false
},
browserConsoleLogOptions: {
level: "error",
format: "%b %T: %m",
terminal: true
},
coverageIstanbulReporter: { coverageIstanbulReporter: {
fixWebpackSourcePaths: true, fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS ? dir: process.env.CIRCLE_ARTIFACTS
process.env.CIRCLE_ARTIFACTS + '/coverage' : ? process.env.CIRCLE_ARTIFACTS + '/coverage'
"dist/reports/coverage", : "dist/reports/coverage",
reports: ['html', 'lcovonly', 'text-summary'], reports: ['html', 'lcovonly', 'text-summary'],
thresholds: { thresholds: {
global: { global: {

96
lighthouserc.yml Normal file
View File

@ -0,0 +1,96 @@
---
ci:
collect:
urls:
- http://localhost/
numberOfRuns: 5
settings:
onlyCategories:
- performance
- best-practices
upload:
target: temporary-public-storage
assert:
preset: lighthouse:recommended
assertions:
### Applicable assertions
bootup-time:
- warn
- minScore: 0.88 #Original value was calculated at 0.88
dom-size:
- error
- maxNumericValue: 200 #Original value was calculated at 188
first-contentful-paint:
- error
- minScore: 0.07 #Original value was calculated at 0.08
mainthread-work-breakdown:
- warn
- minScore: 0.8 #Original value was calculated at 0.8
unused-javascript:
- warn
- maxLength: 1
- error
- maxNumericValue: 2000 #Original value was calculated at 1855
unused-css-rules: warn
installable-manifest: warn
service-worker: warn
### Disabled seo, accessibility, and pwa assertions, below
categories:seo: 'off'
categories:accessibility: 'off'
categories:pwa: 'off'
accesskeys: 'off'
apple-touch-icon: 'off'
aria-allowed-attr: 'off'
aria-command-name: 'off'
aria-hidden-body: 'off'
aria-hidden-focus: 'off'
aria-input-field-name: 'off'
aria-meter-name: 'off'
aria-progressbar-name: 'off'
aria-required-attr: 'off'
aria-required-children: 'off'
aria-required-parent: 'off'
aria-roles: 'off'
aria-toggle-field-name: 'off'
aria-tooltip-name: 'off'
aria-treeitem-name: 'off'
aria-valid-attr: 'off'
aria-valid-attr-value: 'off'
button-name: 'off'
bypass: 'off'
canonical: 'off'
color-contrast: 'off'
content-width: 'off'
crawlable-anchors: 'off'
csp-xss: 'off'
font-display: 'off'
font-size: 'off'
maskable-icon: 'off'
heading-order: 'off'
hreflang: 'off'
html-has-lang: 'off'
html-lang-valid: 'off'
http-status-code: 'off'
image-alt: 'off'
input-image-alt: 'off'
is-crawlable: 'off'
label: 'off'
link-name: 'off'
link-text: 'off'
list: 'off'
listitem: 'off'
meta-description: 'off'
meta-refresh: 'off'
meta-viewport: 'off'
object-alt: 'off'
plugins: 'off'
robots-txt: 'off'
splash-screen: 'off'
tabindex: 'off'
tap-targets: 'off'
td-headers-attr: 'off'
th-has-data-cells: 'off'
themed-omnibox: 'off'
valid-lang: 'off'
video-caption: 'off'
viewport: 'off'

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.3-SNAPSHOT", "version": "1.7.6-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
@ -34,20 +34,21 @@
"git-rev-sync": "^1.4.0", "git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0", "glob": ">= 3.0.0",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html2canvas": "^1.0.0-alpha.12", "html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0", "imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1", "istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0", "jasmine-core": "^3.7.1",
"jsdoc": "^3.3.2", "jsdoc": "^3.3.2",
"karma": "5.1.1", "karma": "6.3.4",
"karma-chrome-launcher": "3.1.0", "karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0", "karma-firefox-launcher": "2.1.1",
"karma-cli": "2.0.0", "karma-cli": "2.0.0",
"karma-coverage": "2.0.3", "karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-junit-reporter": "2.0.1",
"karma-html-reporter": "0.2.7", "karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1", "karma-jasmine": "4.0.1",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2", "karma-webpack": "4.0.2",
"location-bar": "^3.0.1", "location-bar": "^3.0.1",
"lodash": "^4.17.12", "lodash": "^4.17.12",
@ -60,7 +61,7 @@
"moment-timezone": "0.5.28", "moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3", "node-bourbon": "^4.2.3",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"painterro": "^1.0.35", "painterro": "^1.2.56",
"printj": "^1.2.1", "printj": "^1.2.1",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"request": "^2.69.0", "request": "^2.69.0",
@ -89,6 +90,7 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run", "test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'", "verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
@ -100,6 +102,9 @@
"type": "git", "type": "git",
"url": "https://github.com/nasa/openmct.git" "url": "https://github.com/nasa/openmct.git"
}, },
"engines": {
"node": ">=10.10.2 <16.0.0"
},
"author": "", "author": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true "private": true

View File

@ -24,7 +24,6 @@ define([
"./src/navigation/NavigationService", "./src/navigation/NavigationService",
"./src/navigation/NavigateAction", "./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler", "./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./res/templates/browse.html", "./res/templates/browse.html",
"./res/templates/browse-object.html", "./res/templates/browse-object.html",
"./res/templates/browse/object-header.html", "./res/templates/browse/object-header.html",
@ -37,7 +36,6 @@ define([
NavigationService, NavigationService,
NavigateAction, NavigateAction,
OrphanNavigationHandler, OrphanNavigationHandler,
NewTabAction,
browseTemplate, browseTemplate,
browseObjectTemplate, browseObjectTemplate,
objectHeaderTemplate, objectHeaderTemplate,
@ -128,23 +126,6 @@ define([
"depends": [ "depends": [
"navigationService" "navigationService"
] ]
},
{
"key": "window",
"name": "Open In New Tab",
"implementation": NewTabAction,
"description": "Open in a new browser tab",
"category": [
"view-control",
"contextual"
],
"depends": [
"urlService",
"$window"
],
"group": "windowing",
"priority": 10,
"cssClass": "icon-new-window"
} }
], ],
"runs": [ "runs": [

View File

@ -1,75 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/windowing/NewTabAction"],
function (NewTabAction) {
describe("The new tab action", function () {
var actionSelected,
actionCurrent,
mockWindow,
mockContextCurrent,
mockContextSelected,
mockUrlService;
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ["open", "location"]);
// Context if the current object is selected
// For example, when the top right new tab
// button is clicked, the user is using the
// current domainObject
mockContextCurrent = jasmine.createSpyObj("context", ["domainObject"]);
// Context if the selected object is selected
// For example, when an object in the left
// tree is opened in a new tab using the
// context menu
mockContextSelected = jasmine.createSpyObj("context", ["selectedObject",
"domainObject"]);
// Mocks the urlService used to make the new tab's url from a
// domainObject and mode
mockUrlService = jasmine.createSpyObj("urlService", ["urlForNewTab"]);
// Action done using the current context or mockContextCurrent
actionCurrent = new NewTabAction(mockUrlService, mockWindow,
mockContextCurrent);
// Action done using the selected context or mockContextSelected
actionSelected = new NewTabAction(mockUrlService, mockWindow,
mockContextSelected);
});
it("new tab with current url is opened", function () {
actionCurrent.perform();
});
it("new tab with a selected url is opened", function () {
actionSelected.perform();
});
});
}
);

View File

@ -21,32 +21,24 @@
*****************************************************************************/ *****************************************************************************/
define([ define([
"moment-timezone",
"./src/indicators/ClockIndicator",
"./src/services/TickerService", "./src/services/TickerService",
"./src/services/TimerService", "./src/services/TimerService",
"./src/controllers/ClockController",
"./src/controllers/TimerController", "./src/controllers/TimerController",
"./src/controllers/RefreshingController", "./src/controllers/RefreshingController",
"./src/actions/StartTimerAction", "./src/actions/StartTimerAction",
"./src/actions/RestartTimerAction", "./src/actions/RestartTimerAction",
"./src/actions/StopTimerAction", "./src/actions/StopTimerAction",
"./src/actions/PauseTimerAction", "./src/actions/PauseTimerAction",
"./res/templates/clock.html",
"./res/templates/timer.html" "./res/templates/timer.html"
], function ( ], function (
MomentTimezone,
ClockIndicator,
TickerService, TickerService,
TimerService, TimerService,
ClockController,
TimerController, TimerController,
RefreshingController, RefreshingController,
StartTimerAction, StartTimerAction,
RestartTimerAction, RestartTimerAction,
StopTimerAction, StopTimerAction,
PauseTimerAction, PauseTimerAction,
clockTemplate,
timerTemplate timerTemplate
) { ) {
return { return {
@ -73,16 +65,6 @@ define([
"value": "YYYY/MM/DD HH:mm:ss" "value": "YYYY/MM/DD HH:mm:ss"
} }
], ],
"indicators": [
{
"implementation": ClockIndicator,
"depends": [
"tickerService",
"CLOCK_INDICATOR_FORMAT"
],
"priority": "preferred"
}
],
"services": [ "services": [
{ {
"key": "tickerService", "key": "tickerService",
@ -99,14 +81,6 @@ define([
} }
], ],
"controllers": [ "controllers": [
{
"key": "ClockController",
"implementation": ClockController,
"depends": [
"$scope",
"tickerService"
]
},
{ {
"key": "TimerController", "key": "TimerController",
"implementation": TimerController, "implementation": TimerController,
@ -126,12 +100,6 @@ define([
} }
], ],
"views": [ "views": [
{
"key": "clock",
"type": "clock",
"editable": false,
"template": clockTemplate
},
{ {
"key": "timer", "key": "timer",
"type": "timer", "type": "timer",
@ -181,75 +149,11 @@ define([
], ],
"category": "contextual", "category": "contextual",
"name": "Stop", "name": "Stop",
"cssClass": "icon-box", "cssClass": "icon-box-round-corners",
"priority": "preferred" "priority": "preferred"
} }
], ],
"types": [ "types": [
{
"key": "clock",
"name": "Clock",
"cssClass": "icon-clock",
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
"priority": 101,
"features": [
"creation"
],
"properties": [
{
"key": "clockFormat",
"name": "Display Format",
"control": "composite",
"items": [
{
"control": "select",
"options": [
{
"value": "YYYY/MM/DD hh:mm:ss",
"name": "YYYY/MM/DD hh:mm:ss"
},
{
"value": "YYYY/DDD hh:mm:ss",
"name": "YYYY/DDD hh:mm:ss"
},
{
"value": "hh:mm:ss",
"name": "hh:mm:ss"
}
],
"cssClass": "l-inline"
},
{
"control": "select",
"options": [
{
"value": "clock12",
"name": "12hr"
},
{
"value": "clock24",
"name": "24hr"
}
],
"cssClass": "l-inline"
}
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": MomentTimezone.tz.names()
}
],
"model": {
"clockFormat": [
"YYYY/MM/DD hh:mm:ss",
"clock12"
],
"timezone": "UTC"
}
},
{ {
"key": "timer", "key": "timer",
"name": "Timer", "name": "Timer",

View File

@ -1,110 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
define([
'moment',
'moment-timezone'
],
function (
moment,
momentTimezone
) {
/**
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var lastTimestamp,
unlisten,
timeFormat,
zoneName,
self = this;
function update() {
var m = zoneName
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
self.zoneAbbr = m.zoneAbbr();
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
lastTimestamp = timestamp;
update();
}
function updateModel(model) {
var baseFormat;
if (model !== undefined) {
baseFormat = model.clockFormat[0];
self.use24 = model.clockFormat[1] === 'clock24';
timeFormat = self.use24
? baseFormat.replace('hh', "HH") : baseFormat;
// If wrong timezone is provided, the UTC will be used
zoneName = momentTimezone.tz.names().includes(model.timezone)
? model.timezone : "UTC";
update();
}
}
// Pull in the model (clockFormat and timezone) from the domain object model
$scope.$watch('model', updateModel);
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
}
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
ClockController.prototype.zone = function () {
return this.zoneAbbr;
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
return ClockController;
}
);

View File

@ -1,65 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
define(
['moment'],
function (moment) {
/**
* Indicator that displays the current UTC time in the status area.
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
}
ClockIndicator.prototype.getGlyphClass = function () {
return "";
};
ClockIndicator.prototype.getCssClass = function () {
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}
);

View File

@ -1,107 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2017, 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.
*****************************************************************************/
define(
["../../src/controllers/ClockController"],
function (ClockController) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000;
describe("A clock view's controller", function () {
var mockScope,
mockTicker,
mockUnticker,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
controller = new ClockController(mockScope, mockTicker);
});
it("watches for model (clockFormat and timezone) from the domain object model", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model",
jasmine.any(Function)
);
});
it("subscribes to clock ticks", function () {
expect(mockTicker.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("unsubscribes to ticks when destroyed", function () {
// Make sure $destroy is being listened for...
expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy');
expect(mockUnticker).not.toHaveBeenCalled();
// ...and makes sure that its listener unsubscribes from ticker
mockScope.$on.calls.mostRecent().args[1]();
expect(mockUnticker).toHaveBeenCalled();
});
it("formats using the format string from the model", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock24"
],
"timezone": "Canada/Eastern"
});
expect(controller.zone()).toEqual("EDT");
expect(controller.text()).toEqual("2015-154 13:56:14");
expect(controller.ampm()).toEqual("");
});
it("formats 12-hour time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock12"
],
"timezone": ""
});
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 05:56:14");
expect(controller.ampm()).toEqual("PM");
});
it("does not throw exceptions when model is undefined", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(function () {
mockScope.$watch.calls.mostRecent().args[1](undefined);
}).not.toThrow();
});
});
}
);

View File

@ -101,7 +101,7 @@ define(
name: "Pause" name: "Pause"
}); });
mockStop.getMetadata.and.returnValue({ mockStop.getMetadata.and.returnValue({
cssClass: "icon-box", cssClass: "icon-box-round-corners",
name: "Stop" name: "Stop"
}); });
mockScope.domainObject = mockDomainObject; mockScope.domainObject = mockDomainObject;

View File

@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
define(
["../../src/indicators/ClockIndicator"],
function (ClockIndicator) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000,
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
describe("The clock indicator", function () {
var mockTicker,
mockUnticker,
indicator;
beforeEach(function () {
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
});
it("displays the current time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
});
it("implements the Indicator interface", function () {
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
});
}
);

View File

@ -1,120 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
define([
'./src/HyperlinkController',
'./res/templates/hyperlink.html'
], function (
HyperlinkController,
hyperlinkTemplate
) {
return {
name: "platform/features/hyperlink",
definition: {
"name": "Hyperlink",
"description": "Insert a hyperlink to reference a link",
"extensions": {
"types": [
{
"key": "hyperlink",
"name": "Hyperlink",
"cssClass": "icon-chain-links",
"description": "A hyperlink to redirect to a different link",
"features": ["creation"],
"properties": [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"value": "button",
"name": "Button"
}
],
"cssClass": "l-inline"
},
{
"key": "openNewTab",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "thisTab"
},
{
"value": "newTab",
"name": "Open in a new tab"
}
],
"cssClass": "l-inline"
}
],
"model": {
"displayFormat": "link",
"openNewTab": "thisTab",
"removeTitle": true
}
}
],
"views": [
{
"key": "hyperlink",
"type": "hyperlink",
"name": "Hyperlink Display",
"template": hyperlinkTemplate,
"editable": false
}
],
"controllers": [
{
"key": "HyperlinkController",
"implementation": HyperlinkController,
"depends": ["$scope"]
}
]
}
}
};
});

View File

@ -1,61 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* This bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
create a new tab to open the link in
* @namespace platform/features/hyperlink
*/
define(
[],
function () {
function HyperlinkController($scope) {
this.$scope = $scope;
}
/**Function to analyze the location in which to open the hyperlink
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
**/
HyperlinkController.prototype.openNewTab = function () {
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
return false;
} else {
return true;
}
};
/**Function to specify the format in which the hyperlink should be created
@returns true if the hyperlink is chosen to be created as a button, false if a link
**/
HyperlinkController.prototype.isButton = function () {
if (this.$scope.domainObject.getModel().displayFormat === "link") {
return false;
}
return true;
};
return HyperlinkController;
}
);

View File

@ -1,89 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
define(
["../src/HyperlinkController"],
function (HyperlinkController) {
describe("The controller for hyperlinks", function () {
var domainObject,
controller,
scope;
beforeEach(function () {
scope = jasmine.createSpyObj(
"$scope",
["domainObject"]
);
domainObject = jasmine.createSpyObj(
"domainObject",
["getModel"]
);
scope.domainObject = domainObject;
controller = new HyperlinkController(scope);
});
it("knows when it should open a new tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "newTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(true);
});
it("knows when it is a button", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "button",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.isButton())
.toEqual(true);
});
it("knows when it should open in the same tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
it("knows when it is a link", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
});
}
);

View File

@ -1,70 +0,0 @@
This bundle provides the Timeline domain object type, as well
as other associated domain object types and relevant views.
# Implementation notes
## Model Properties
The properties below record properties relevant to using and
understanding timelines based on their JSON representation.
Additional common properties, such as `modified`
or `persisted` timestamps, may also be present.
### Timeline Model
A timeline's model looks like:
```
{
"type": "timeline",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"capacity": <number> (optional; battery capacity in watt-hours)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should refer to
other Timeline objects, or to Activity objects.
### Activity Model
An activity's model looks like:
```
{
"type": "activity",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"duration": {
"timestamp": <number> (duration of this activity, in milliseconds)
"epoch": "SET" (this is ignored)
},
"relationships": {
"modes": <string[]> (array of applicable Activity Mode ids)
},
"link": <string> (optional; URL linking to associated external resource)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should only refer to
other Activity objects.
### Activity Mode Model
An activity mode's model looks like:
```
{
"type": "mode",
"resources": {
"comms": <number> (communications utilization, in Kbps)
"power": <number> (power utilization, in watts)
}
}
```

View File

@ -1,10 +0,0 @@
<div>
Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported.
</div>
<div>
Please open an issue in the
<a href="https://github.com/nasa/openmct/issues" target="_blank">
Open MCT Issue tracker
</a>
if you have any questions about the timeline plugin.
</div>

View File

@ -122,6 +122,7 @@ define([
} }
}; };
this.destroy = this.destroy.bind(this);
/** /**
* Tracks current selection state of the application. * Tracks current selection state of the application.
* @private * @private
@ -262,7 +263,7 @@ define([
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable()); this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin()); this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default()); this.install(LicensesPlugin.default());
@ -274,6 +275,7 @@ define([
this.install(ImageryPlugin.default()); this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout()); this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction()); this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.ImportExport()); this.install(this.plugins.ImportExport());
this.install(this.plugins.WebPage()); this.install(this.plugins.WebPage());
this.install(this.plugins.Condition()); this.install(this.plugins.Condition());
@ -282,6 +284,7 @@ define([
this.install(this.plugins.NotificationIndicator()); this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction()); this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction()); this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors()); this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder()); this.install(this.plugins.NonEditableFolder());
} }
@ -433,6 +436,8 @@ define([
Browse(this); Browse(this);
} }
window.addEventListener('beforeunload', this.destroy);
this.router.start(); this.router.start();
this.emit('start'); this.emit('start');
}.bind(this)); }.bind(this));
@ -456,6 +461,7 @@ define([
}; };
MCT.prototype.destroy = function () { MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy'); this.emit('destroy');
this.router.destroy(); this.router.destroy();
}; };

View File

@ -36,8 +36,7 @@ define([
'./views/installLegacyViews', './views/installLegacyViews',
'./policies/LegacyCompositionPolicyAdapter', './policies/LegacyCompositionPolicyAdapter',
'./actions/LegacyActionAdapter', './actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter', './services/LegacyPersistenceAdapter'
'./services/ExportImageService'
], function ( ], function (
ActionDialogDecorator, ActionDialogDecorator,
AdapterCapability, AdapterCapability,
@ -54,8 +53,7 @@ define([
installLegacyViews, installLegacyViews,
legacyCompositionPolicyAdapter, legacyCompositionPolicyAdapter,
LegacyActionAdapter, LegacyActionAdapter,
LegacyPersistenceAdapter, LegacyPersistenceAdapter
ExportImageService
) { ) {
return { return {
name: 'src/adapter', name: 'src/adapter',
@ -84,13 +82,6 @@ define([
"identifierService", "identifierService",
"cacheService" "cacheService"
] ]
},
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
} }
], ],
components: [ components: [

View File

@ -173,10 +173,11 @@ define([
const limitEvaluator = oldObject.getCapability("limit"); const limitEvaluator = oldObject.getCapability("limit");
return { return {
limits: function () { limits: () => {
return limitEvaluator.limits(); return limitEvaluator.limits.then !== undefined
? limitEvaluator.limits()
: Promise.resolve(limitEvaluator.limits());
} }
}; };
}; };

View File

@ -1,186 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
*/
define(
[
"html2canvas",
"saveAs"
],
function (
html2canvas,
{ saveAs }
) {
/**
* The export image service will export any HTML node to
* JPG, or PNG.
* @param {object} dialogService
* @constructor
*/
function ExportImageService(dialogService) {
this.dialogService = dialogService;
this.exportCount = 0;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {string} type of image to convert the element to.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, imageType, className) {
const dialogService = this.dialogService;
const dialog = dialogService.showBlockingMessage({
title: "Capturing...",
hint: "Capturing an image",
unknownProgress: true,
severity: "info",
delay: true
});
let mimeType = "image/png";
if (imageType === "jpg") {
mimeType = "image/jpeg";
}
let exportId = undefined;
let oldId = undefined;
if (className) {
exportId = 'export-element-' + this.exportCount;
this.exportCount++;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
return canvas.toBlob(resolve, mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = dialogService.showBlockingMessage({
title: "Error capturing image",
severity: "error",
hint: "Image was not captured successfully!",
options: [{
label: "OK",
callback: function () {
errorDialog.dismiss();
}
}]
});
});
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "jpg", className).then(function (img) {
saveAs(img, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "png", className).then(function (img) {
saveAs(img, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPNGtoSRC = function (element, className) {
return this.renderElement(element, "png", className);
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
*/
function polyfillToBlob() {
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
value: function (callback, mimeType, quality) {
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
const len = binStr.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: mimeType || "image/png"}));
}
});
}
}
polyfillToBlob();
return ExportImageService;
}
);

View File

@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter {
this._observeObjectPath(); this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions); this.openmct.editor.on('isEditing', this._updateActions);
} }
this._initializeActions();
} }
disable(actionKeys) { disable(actionKeys) {
@ -156,19 +154,10 @@ class ActionCollection extends EventEmitter {
}); });
} }
_initializeActions() {
Object.keys(this.applicableActions).forEach(key => {
this.applicableActions[key].callBack = () => {
return this.applicableActions[key].invoke(this.objectPath, this.view);
};
});
}
_updateActions() { _updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._initializeActions();
this._update(); this._update();
} }

View File

@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this); this.register = this.register.bind(this);
this.get = this.get.bind(this); this.getActionsCollection = this.getActionsCollection.bind(this);
this._applicableActions = this._applicableActions.bind(this); this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
} }
@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter {
this._allActions[actionDefinition.key] = actionDefinition; this._allActions[actionDefinition.key] = actionDefinition;
} }
get(objectPath, view) { getAction(key) {
if (view) { return this._allActions[key];
}
getActionsCollection(objectPath, view) {
if (view) {
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else { } else {
return this._newActionCollection(objectPath, view, true); return this._newActionCollection(objectPath, view, true);
} }
} }
@ -57,15 +59,6 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = groupArray; this._groupOrder = groupArray;
} }
_get(objectPath, view) {
let actionCollection = this._newActionCollection(objectPath, view);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
_getCachedActionCollection(objectPath, view) { _getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view); let cachedActionCollection = this._actionCollections.get(view);
@ -75,7 +68,17 @@ class ActionsAPI extends EventEmitter {
_newActionCollection(objectPath, view, skipEnvironmentObservers) { _newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view); let applicableActions = this._applicableActions(objectPath, view);
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers); const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
if (view) {
this._cacheActionCollection(view, actionCollection);
}
return actionCollection;
}
_cacheActionCollection(view, actionCollection) {
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
} }
_updateCachedActionCollections(key) { _updateCachedActionCollections(key) {

View File

@ -106,7 +106,7 @@ describe('The Actions API', () => {
it("adds action to ActionsAPI", () => { it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction); actionsAPI.register(mockAction);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key]; let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key); expect(action.key).toEqual(mockAction.key);
@ -121,21 +121,21 @@ describe('The Actions API', () => {
}); });
it("returns an ActionCollection when invoked with an objectPath only", () => { it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath); let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection; let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue(); expect(instanceOfActionCollection).toBeTrue();
}); });
it("returns an ActionCollection when invoked with an objectPath and view", () => { it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection; let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue(); expect(instanceOfActionCollection).toBeTrue();
}); });
it("returns relevant actions when invoked with objectPath only", () => { it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath); let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key]; let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
expect(action.key).toEqual(mockObjectPathAction.key); expect(action.key).toEqual(mockObjectPathAction.key);
@ -143,7 +143,7 @@ describe('The Actions API', () => {
}); });
it("returns relevant actions when invoked with objectPath and view", () => { it("returns relevant actions when invoked with objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key]; let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key); expect(action.key).toEqual(mockAction.key);

View File

@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
* @property {Boolean} isDisabled adds disable class if true * @property {Boolean} isDisabled adds disable class if true
* @property {String} name Menu item text * @property {String} name Menu item text
* @property {String} description Menu item description * @property {String} description Menu item description
* @property {Function} callBack callback function: invoked when item is clicked * @property {Function} onItemClicked callback function: invoked when item is clicked
*/ */
/** /**
@ -66,12 +66,27 @@ class MenuAPI {
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action} * @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/ */
showMenu(x, y, actions, menuOptions) { showMenu(x, y, items, menuOptions) {
this._createMenuComponent(x, y, actions, menuOptions); this._createMenuComponent(x, y, items, menuOptions);
this.menuComponent.showMenu(); this.menuComponent.showMenu();
} }
actionsToMenuItems(actions, objectPath, view) {
return actions.map(action => {
const isActionGroup = Array.isArray(action);
if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view);
} else {
action.onItemClicked = () => {
action.invoke(objectPath, view);
};
}
return action;
});
}
/** /**
* Show popup menu with description of item on hover * Show popup menu with description of item on hover
* @param {number} x x-coordinates for popup * @param {number} x x-coordinates for popup

View File

@ -57,7 +57,7 @@ describe ('The Menu API', () => {
name: 'Test Action 1', name: 'Test Action 1',
cssClass: 'icon-clock', cssClass: 'icon-clock',
description: 'This is a test action', description: 'This is a test action',
callBack: () => { onItemClicked: () => {
result = 'Test Action 1 Invoked'; result = 'Test Action 1 Invoked';
} }
}, },
@ -66,7 +66,7 @@ describe ('The Menu API', () => {
name: 'Test Action 2', name: 'Test Action 2',
cssClass: 'icon-clock', cssClass: 'icon-clock',
description: 'This is a test action', description: 'This is a test action',
callBack: () => { onItemClicked: () => {
result = 'Test Action 2 Invoked'; result = 'Test Action 2 Invoked';
} }
} }

View File

@ -11,7 +11,7 @@
:key="action.name" :key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description" :title="action.description"
@click="action.callBack" @click="action.onItemClicked"
> >
{{ action.name }} {{ action.name }}
</li> </li>
@ -36,7 +36,7 @@
:key="action.name" :key="action.name"
:class="action.cssClass" :class="action.cssClass"
:title="action.description" :title="action.description"
@click="action.callBack" @click="action.onItemClicked"
> >
{{ action.name }} {{ action.name }}
</li> </li>

View File

@ -13,7 +13,7 @@
:key="action.name" :key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description" :title="action.description"
@click="action.callBack" @click="action.onItemClicked"
@mouseover="toggleItemDescription(action)" @mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()" @mouseleave="toggleItemDescription()"
> >
@ -42,7 +42,7 @@
:key="action.name" :key="action.name"
:class="action.cssClass" :class="action.cssClass"
:title="action.description" :title="action.description"
@click="action.callBack" @click="action.onItemClicked"
@mouseover="toggleItemDescription(action)" @mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()" @mouseleave="toggleItemDescription()"
> >

View File

@ -71,12 +71,12 @@ class Menu extends EventEmitter {
showMenu() { showMenu() {
this.component = new Vue({ this.component = new Vue({
provide: {
options: this.options
},
components: { components: {
MenuComponent MenuComponent
}, },
provide: {
options: this.options
},
template: '<menu-component />' template: '<menu-component />'
}); });
@ -85,12 +85,12 @@ class Menu extends EventEmitter {
showSuperMenu() { showSuperMenu() {
this.component = new Vue({ this.component = new Vue({
provide: {
options: this.options
},
components: { components: {
SuperMenuComponent SuperMenuComponent
}, },
provide: {
options: this.options
},
template: '<super-menu-component />' template: '<super-menu-component />'
}); });

View File

@ -45,6 +45,8 @@ function ObjectAPI(typeRegistry, openmct) {
this.rootProvider = new RootObjectProvider(this.rootRegistry); this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {}; this.cache = {};
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
} }
/** /**
@ -397,21 +399,26 @@ ObjectAPI.prototype._toMutable = function (object) {
mutableObject = object; mutableObject = object;
} else { } else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter); mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
}
// Check if provider supports realtime updates // Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier); let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier); let provider = this.getProvider(identifier);
if (provider !== undefined if (provider !== undefined
&& provider.observe !== undefined) { && provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => { let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel); mutableObject.$refresh(updatedModel);
}
}); });
mutableObject.$on('$destroy', () => { mutableObject.$on('$_destroy', () => {
unobserve(); unobserve();
}); });
} }
}
return mutableObject; return mutableObject;
}; };

View File

@ -163,14 +163,22 @@ describe("The Object API", () => {
key: 'test-key' key: 'test-key'
}, },
name: 'test object', name: 'test object',
type: 'notebook',
otherAttribute: 'other-attribute-value', otherAttribute: 'other-attribute-value',
modified: 0,
persisted: 0,
objectAttribute: { objectAttribute: {
embeddedObject: { embeddedObject: {
embeddedKey: 'embedded-value' embeddedKey: 'embedded-value'
} }
} }
}; };
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject); updatedTestObject = Object.assign({
otherAttribute: 'changed-attribute-value'
}, testObject);
updatedTestObject.modified = 1;
updatedTestObject.persisted = 1;
mockProvider = jasmine.createSpyObj("mock provider", [ mockProvider = jasmine.createSpyObj("mock provider", [
"get", "get",
"create", "create",
@ -182,6 +190,8 @@ describe("The Object API", () => {
mockProvider.observeObjectChanges.and.callFake(() => { mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject); callbacks[0](updatedTestObject);
callbacks.splice(0, 1); callbacks.splice(0, 1);
return () => {};
}); });
mockProvider.observe.and.callFake((id, callback) => { mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) { if (callbacks.length === 0) {
@ -189,6 +199,8 @@ describe("The Object API", () => {
} else { } else {
callbacks[0] = callback; callbacks[0] = callback;
} }
return () => {};
}); });
objectAPI.addProvider(TEST_NAMESPACE, mockProvider); objectAPI.addProvider(TEST_NAMESPACE, mockProvider);

View File

@ -21,8 +21,7 @@
&__outer { &__outer {
@include abs(); @include abs();
background: $overlayColorBg; background: $colorBodyBg;
color: $overlayColorFg;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: $overlayInnerMargin; padding: $overlayInnerMargin;
@ -30,7 +29,6 @@
&__close-button { &__close-button {
$p: $interiorMargin + 2px; $p: $interiorMargin + 2px;
color: $overlayColorFg;
font-size: 1.5em; font-size: 1.5em;
position: absolute; position: absolute;
top: $p; right: $p; top: $p; right: $p;
@ -82,11 +80,6 @@
} }
} }
.c-button,
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name { .c-object-label__name {
filter: $objectLabelNameFilter; filter: $objectLabelNameFilter;
} }
@ -103,6 +96,7 @@ body.desktop {
} }
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element. // Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small, .l-overlay-small,
.l-overlay-fit { .l-overlay-fit {
.c-overlay__outer { .c-overlay__outer {
@ -124,12 +118,8 @@ body.desktop {
$tbPad: floor($pad * 0.8); $tbPad: floor($pad * 0.8);
$lrPad: $pad; $lrPad: $pad;
.c-overlay { .c-overlay {
&__blocker {
display: none;
}
&__outer { &__outer {
@include overlaySizing($overlayOuterMarginFullscreen); @include overlaySizing($overlayOuterMarginLarge);
padding: $tbPad $lrPad; padding: $tbPad $lrPad;
} }

View File

@ -567,21 +567,24 @@ define([
* @method limits returns a limits object of * @method limits returns a limits object of
* type { * type {
* level1: { * level1: {
* low: { key1: value1, key2: value2 }, * low: { key1: value1, key2: value2, color: <supportedColor> },
* high: { key1: value1, key2: value2 } * high: { key1: value1, key2: value2, color: <supportedColor> }
* }, * },
* level2: { * level2: {
* low: { key1: value1, key2: value2 }, * low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 } * high: { key1: value1, key2: value2 }
* } * }
* } * }
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimits = function (domainObject) { TelemetryAPI.prototype.getLimits = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider) { if (!provider || !provider.getLimits) {
return { return {
limits: function () {} limits: function () {
return Promise.resolve(undefined);
}
}; };
} }

View File

@ -0,0 +1,185 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Class defining an image exporter for JPG/PNG output.
* Originally created by hudsonfoo on 09/02/16
*/
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
import {saveAs} from 'file-saver/FileSaver';
import html2canvas from 'html2canvas';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
this.openmct = openmct;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
renderElement(element, { imageType, className, thumbnailSize }) {
const self = this;
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: function () {
dialog.dismiss();
}
}
]
});
let mimeType = 'image/png';
if (imageType === 'jpg') {
mimeType = 'image/jpeg';
}
let exportId = undefined;
let oldId = undefined;
if (className) {
const newUUID = uuid();
exportId = `$export-element-${newUUID}`;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',
buttons: [
{
label: "OK",
emphasis: true,
callback: function () {
errorDialog.dismiss();
}
}
]
});
});
}
getThumbnail(canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
}
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'png',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
exportPNGtoSRC(element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
}
}
export default ImageExporter;

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageExporter from './ImageExporter';
import { createOpenMct, resetApplicationState } from '../utils/testing';
describe('The Image Exporter', () => {
let openmct;
let imageExporter;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("basic instatation", () => {
it("can be instatiated", () => {
imageExporter = new ImageExporter(openmct);
expect(imageExporter).not.toEqual(null);
});
it("can render an element to a blob", async () => {
const mockHeadElement = document.createElement("h1");
const mockTextNode = document.createTextNode('foo bar');
mockHeadElement.appendChild(mockTextNode);
document.body.appendChild(mockHeadElement);
imageExporter = new ImageExporter(openmct);
const returnedBlob = await imageExporter.renderElement(document.body, {
imageType: 'png'
});
expect(returnedBlob).not.toEqual(null);
expect(returnedBlob.blob).not.toEqual(null);
expect(returnedBlob.blob).toBeInstanceOf(Blob);
});
});
});

View File

@ -38,8 +38,6 @@ const DEFAULTS = [
'platform/exporters', 'platform/exporters',
'platform/telemetry', 'platform/telemetry',
'platform/features/clock', 'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms', 'platform/forms',
'platform/identity', 'platform/identity',
'platform/persistence/aggregator', 'platform/persistence/aggregator',
@ -82,9 +80,7 @@ define([
'../platform/exporters/bundle', '../platform/exporters/bundle',
'../platform/features/clock/bundle', '../platform/features/clock/bundle',
'../platform/features/my-items/bundle', '../platform/features/my-items/bundle',
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle', '../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle', '../platform/forms/bundle',
'../platform/framework/bundle', '../platform/framework/bundle',
'../platform/framework/src/load/Bundle', '../platform/framework/src/load/Bundle',

View File

@ -73,7 +73,7 @@ describe('the plugin', function () {
}); });
it('provides a folder to hold plans', () => { it('provides a folder to hold plans', () => {
openmct.objects.get(identifier).then((object) => { return openmct.objects.get(identifier).then((object) => {
expect(object).toEqual({ expect(object).toEqual({
identifier, identifier,
type: 'folder', type: 'folder',
@ -83,7 +83,7 @@ describe('the plugin', function () {
}); });
it('provides composition for couch search folders', () => { it('provides composition for couch search folders', () => {
composition.load().then((objects) => { return composition.load().then((objects) => {
expect(objects.length).toEqual(2); expect(objects.length).toEqual(2);
}); });
}); });

View File

@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue'; import LadTableSetView from './LadTableSetView';
export default function LADTableSetViewProvider(openmct) { export default function LADTableSetViewProvider(openmct) {
return { return {
@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) {
return domainObject.type === 'LadTableSet'; return domainObject.type === 'LadTableSet';
}, },
view: function (domainObject, objectPath) { view: function (domainObject, objectPath) {
let component; return new LadTableSetView(openmct, domainObject, objectPath);
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableSet: LadTableSet
},
provide: {
openmct,
objectPath
},
data() {
return {
domainObject
};
},
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
}, },
priority: function () { priority: function () {
return 1; return 1;

View File

@ -0,0 +1,45 @@
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
export default class LADTableView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTable
},
provide: {
openmct: this.openmct,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
};
},
template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTable.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@ -19,50 +19,30 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
export default function LADTableViewProvider(openmct) { import LADTableView from './LADTableView';
return {
key: 'LadTable',
name: 'LAD Table',
cssClass: 'icon-tabular-lad',
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject, objectPath) {
let component;
return { export default class LADTableViewProvider {
show: function (element) { constructor(openmct) {
component = new Vue({ this.openmct = openmct;
el: element, this.name = 'LAD Table';
components: { this.key = 'LadTable';
LadTableComponent: LadTable this.cssClass = 'icon-tabular-lad';
},
provide: {
openmct
},
data: () => {
return {
domainObject,
objectPath
};
},
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
} }
};
}, canView(domainObject) {
priority: function () { return domainObject.type === 'LadTable';
}
canEdit(domainObject) {
return domainObject.type === 'LadTable';
}
view(domainObject, objectPath) {
return new LADTableView(this.openmct, domainObject, objectPath);
}
priority(domainObject) {
return 1; return 1;
} }
};
} }

View File

@ -0,0 +1,45 @@
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
export default class LadTableSetView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTableSet
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject
};
},
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTableSet.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [
]; ];
export default { export default {
inject: ['openmct'], inject: ['openmct', 'currentView'],
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -167,25 +167,23 @@ export default {
this.resetValues(); this.resetValues();
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
getView() { updateViewContext() {
return { this.$emit('rowContextClick', {
getViewContext: () => {
return {
viewHistoricalData: true, viewHistoricalData: true,
viewDatumAction: true, viewDatumAction: true,
getDatum: () => { getDatum: () => {
return this.datum; return this.datum;
} }
}; });
}
};
}, },
showContextMenu(event) { showContextMenu(event) {
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView()); this.updateViewContext();
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.openmct.menus.showMenu(event.x, event.y, applicableActions); const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}
}, },
resetValues() { resetValues() {
this.value = '---'; this.value = '---';

View File

@ -38,6 +38,7 @@
:domain-object="ladRow.domainObject" :domain-object="ladRow.domainObject"
:path-to-table="objectPath" :path-to-table="objectPath"
:has-units="hasUnits" :has-units="hasUnits"
@rowContextClick="updateViewContext"
/> />
</tbody> </tbody>
</table> </table>
@ -51,7 +52,7 @@ export default {
components: { components: {
LadRow LadRow
}, },
inject: ['openmct'], inject: ['openmct', 'currentView'],
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -64,7 +65,8 @@ export default {
}, },
data() { data() {
return { return {
items: [] items: [],
viewContext: {}
}; };
}, },
computed: { computed: {
@ -114,6 +116,12 @@ export default {
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit); let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
return metadataWithUnits.length > 0; return metadataWithUnits.length > 0;
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
} }
} }
}; };

View File

@ -48,6 +48,7 @@
:domain-object="ladRow.domainObject" :domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath" :path-to-table="ladTable.objectPath"
:has-units="hasUnits" :has-units="hasUnits"
@rowContextClick="updateViewContext"
/> />
</template> </template>
</tbody> </tbody>
@ -61,7 +62,7 @@ export default {
components: { components: {
LadRow LadRow
}, },
inject: ['openmct', 'objectPath'], inject: ['openmct', 'objectPath', 'currentView'],
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -72,7 +73,8 @@ export default {
return { return {
ladTableObjects: [], ladTableObjects: [],
ladTelemetryObjects: {}, ladTelemetryObjects: {},
compositions: [] compositions: [],
viewContext: {}
}; };
}, },
computed: { computed: {
@ -166,6 +168,12 @@ export default {
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects); this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
}; };
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
} }
} }
}; };

View File

@ -67,10 +67,6 @@ describe("The LAD Table", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
parent = document.createElement('div'); parent = document.createElement('div');
@ -90,7 +86,7 @@ describe("The LAD Table", () => {
}); });
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -113,7 +109,8 @@ describe("The LAD Table", () => {
beforeEach(() => { beforeEach(() => {
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable); ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
ladTableCompositionCollection.load();
return ladTableCompositionCollection.load();
}); });
it("should accept telemetry producing objects", () => { it("should accept telemetry producing objects", () => {
@ -192,8 +189,6 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick(); await Vue.nextTick();
return;
}); });
it("should show one row per object in the composition", () => { it("should show one row per object in the composition", () => {
@ -242,13 +237,6 @@ describe("The LAD Table Set", () => {
let ladPlugin; let ladPlugin;
let parent; let parent;
let child; let child;
let telemetryCount = 3;
let timeFormat = 'utc';
let mockTelemetry = getMockTelemetry({
count: telemetryCount,
format: timeFormat
});
let mockObj = getMockObjects({ let mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry'] objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
@ -264,31 +252,22 @@ describe("The LAD Table Set", () => {
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier); mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
parent = document.createElement('div'); parent = document.createElement('div');
child = document.createElement('div'); child = document.createElement('div');
parent.appendChild(child); parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin(); ladPlugin = new LadPlugin();
openmct.install(ladPlugin); openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({ openmct.time.bounds({
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
}); });
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -301,6 +280,8 @@ describe("The LAD Table Set", () => {
}); });
it("should provide a lad table set view only for lad table set objects", () => { it("should provide a lad table set view only for lad table set objects", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
let ladTableSetView = applicableViews.find( let ladTableSetView = applicableViews.find(
@ -315,8 +296,11 @@ describe("The LAD Table Set", () => {
let ladTableSetCompositionCollection; let ladTableSetCompositionCollection;
beforeEach(() => { beforeEach(() => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
ladTableSetCompositionCollection.load();
return ladTableSetCompositionCollection.load();
}); });
it("should accept lad table objects", () => { it("should accept lad table objects", () => {
@ -354,41 +338,17 @@ describe("The LAD Table Set", () => {
otherObj.ladTable.composition.push(mockObj.telemetry.identifier); otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier); mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
beforeEach(async () => { beforeEach(() => {
let telemetryRequestResolve; spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
let ladObjectResolve;
let anotherLadObjectResolve;
let telemetryRequestPromise = new Promise((resolve) => { spyOn(openmct.objects, 'get').and.callFake((obj) => {
telemetryRequestResolve = resolve;
});
let ladObjectPromise = new Promise((resolve) => {
ladObjectResolve = resolve;
});
let anotherLadObjectPromise = new Promise((resolve) => {
anotherLadObjectResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'lad-object') { if (obj.key === 'lad-object') {
ladObjectResolve(mockObj.ladObject); return Promise.resolve(mockObj.ladTable);
return ladObjectPromise;
} else if (obj.key === 'another-lad-object') { } else if (obj.key === 'another-lad-object') {
anotherLadObjectResolve(otherObj.ladObject); return Promise.resolve(otherObj.ladTable);
} else if (obj.key === 'telemetry-object') {
return anotherLadObjectPromise; return Promise.resolve(mockObj.telemetry);
} }
return Promise.resolve({});
}); });
openmct.time.bounds({ openmct.time.bounds({
@ -399,20 +359,19 @@ describe("The LAD Table Set", () => {
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey); ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child, true); ladTableSetView.show(child);
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]); return Vue.nextTick();
await Vue.nextTick();
return;
}); });
it("should show one row per lad table object in the composition", () => { it("should show one row per lad table object in the composition", () => {
const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
return ladTableSetCompositionCollection.load().then(() => {
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length; const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
expect(rowCount).toBe(mockObj.ladTableSet.composition.length); expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
pending(); });
}); });
}); });
}); });

View File

@ -22,12 +22,14 @@
define( define(
[ [
"utils/testing",
"./URLIndicator", "./URLIndicator",
"./URLIndicatorPlugin", "./URLIndicatorPlugin",
"../../MCT", "../../MCT",
"zepto" "zepto"
], ],
function ( function (
testingUtils,
URLIndicator, URLIndicator,
URLIndicatorPlugin, URLIndicatorPlugin,
MCT, MCT,
@ -44,7 +46,7 @@ define(
beforeEach(function () { beforeEach(function () {
jasmine.clock().install(); jasmine.clock().install();
openmct = new MCT(); openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add'); spyOn(openmct.indicators, 'add');
spyOn($, 'ajax'); spyOn($, 'ajax');
$.ajax.and.callFake(function (options) { $.ajax.and.callFake(function (options) {
@ -55,6 +57,8 @@ define(
afterEach(function () { afterEach(function () {
$.ajax = defaultAjaxFunction; $.ajax = defaultAjaxFunction;
jasmine.clock().uninstall(); jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
}); });
describe("on initialization", function () { describe("on initialization", function () {

View File

@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer {
} }
initialize() { initialize() {
this.updateTimeSettings();
this.openmct.router.on('change:params', this.updateTimeSettings); this.openmct.router.on('change:params', this.updateTimeSettings);
TIME_EVENTS.forEach(event => { TIME_EVENTS.forEach(event => {

View File

@ -28,8 +28,10 @@ import {
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
import Vue from 'vue';
describe("AutoflowTabularPlugin", () => { // TODO lots of its without expects
xdescribe("AutoflowTabularPlugin", () => {
let testType; let testType;
let testObject; let testObject;
let mockmct; let mockmct;
@ -51,7 +53,7 @@ describe("AutoflowTabularPlugin", () => {
}); });
afterEach(() => { afterEach(() => {
resetApplicationState(mockmct); return resetApplicationState(mockmct);
}); });
it("installs a view provider", () => { it("installs a view provider", () => {
@ -101,7 +103,7 @@ describe("AutoflowTabularPlugin", () => {
}); });
} }
beforeEach((done) => { beforeEach(() => {
callbacks = {}; callbacks = {};
spyOnBuiltins(['requestAnimationFrame']); spyOnBuiltins(['requestAnimationFrame']);
@ -180,7 +182,7 @@ describe("AutoflowTabularPlugin", () => {
view = provider.view(testObject); view = provider.view(testObject);
view.show(testContainer); view.show(testContainer);
return done(); return Vue.nextTick();
}); });
afterEach(() => { afterEach(() => {

View File

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

View File

@ -0,0 +1,99 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="l-angular-ov-wrapper">
<div class="u-contents">
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
<div class="c-clock__timezone">
{{ timeZoneAbbr }}
</div>
<div class="c-clock__value">
{{ timeTextValue }}
</div>
<div class="c-clock__ampm">
{{ timeAmPm }}
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import momentTimezone from 'moment-timezone';
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
lastTimestamp: null
};
},
computed: {
configuration() {
return this.domainObject.configuration;
},
baseFormat() {
return this.configuration.baseFormat;
},
use24() {
return this.configuration.use24 === 'clock24';
},
timezone() {
return this.configuration.timezone;
},
timeFormat() {
return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat;
},
zoneName() {
return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC";
},
momentTime() {
return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp);
},
timeZoneAbbr() {
return this.momentTime.zoneAbbr();
},
timeTextValue() {
return this.timeFormat && this.momentTime.format(this.timeFormat);
},
timeAmPm() {
return this.use24 ? '' : this.momentTime.format("A");
}
},
mounted() {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.lastTimestamp = timestamp;
}
}
};
</script>

View File

@ -19,10 +19,46 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}" <template>
ng-class="{ <div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
'c-hyperlink--button u-fills-container' : hyperlink.isButton(), <span class="label c-indicator__label">
'c-hyperlink--link' : !hyperlink.isButton() }"> {{ timeTextValue }}
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span> </span>
</a> </div>
</template>
<script>
import moment from 'moment';
export default {
inject: ['openmct'],
props: {
indicatorFormat: {
type: String,
required: true
}
},
data() {
return {
timeTextValue: null
};
},
mounted() {
this.openmct.on('start', () => {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
});
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
}
}
};
</script>

154
src/plugins/clock/plugin.js Normal file
View File

@ -0,0 +1,154 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ClockViewProvider from './ClockViewProvider';
import ClockIndicator from './components/ClockIndicator.vue';
import momentTimezone from 'moment-timezone';
import Vue from 'vue';
export default function ClockPlugin(options) {
return function install(openmct) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
creatable: true,
cssClass: 'icon-clock',
initialize: function (domainObject) {
domainObject.configuration = {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
};
},
"form": [
{
"key": "displayFormat",
"name": "Display Format",
control: 'select',
options: [
{
value: 'YYYY/MM/DD hh:mm:ss',
name: 'YYYY/MM/DD hh:mm:ss'
},
{
value: 'YYYY/DDD hh:mm:ss',
name: 'YYYY/DDD hh:mm:ss'
},
{
value: 'hh:mm:ss',
name: 'hh:mm:ss'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'baseFormat'
]
},
{
control: 'select',
options: [
{
value: 'clock12',
name: '12hr'
},
{
value: 'clock24',
name: '24hr'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'use24'
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": momentTimezone.tz.names(),
property: [
'configuration',
'timezone'
]
}
]
});
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
if (options && options.enableClockIndicator) {
const clockIndicator = new Vue ({
components: {
ClockIndicator
},
provide: {
openmct
},
data() {
return {
indicatorFormat: CLOCK_INDICATOR_FORMAT
};
},
template: '<ClockIndicator :indicator-format="indicatorFormat"></ClockIndicator>'
});
const indicator = {
element: clockIndicator.$mount().$el,
key: 'clock-indicator'
};
openmct.indicators.add(indicator);
}
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'clock';
},
invoke: (identifier, domainObject) => {
if (domainObject.configuration) {
return domainObject;
}
if (domainObject.clockFormat
&& domainObject.timezone) {
const baseFormat = domainObject.clockFormat[0];
const use24 = domainObject.clockFormat[1];
const timezone = domainObject.timezone;
domainObject.configuration = {
baseFormat,
use24,
timezone
};
openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);
}
return domainObject;
}
});
};
}

View File

@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions = {}; this.subscriptions = {};
this.telemetryObjects = {}; this.telemetryObjects = {};
this.testData = { this.testData = {
conditionTestData: [], conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
applied: false applied: false
}; };
this.initialize(); this.initialize();
@ -154,9 +154,11 @@ export default class ConditionManager extends EventEmitter {
updateConditionDescription(condition) { updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id)); const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
if (found.summary !== condition.description) {
found.summary = condition.description; found.summary = condition.description;
this.persistConditions(); this.persistConditions();
} }
}
initCondition(conditionConfiguration, index) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
@ -414,9 +416,11 @@ export default class ConditionManager extends EventEmitter {
} }
updateTestData(testData) { updateTestData(testData) {
if (!_.isEqual(testData, this.testData)) {
this.testData = testData; this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
} }
}
persistConditions() { persistConditions() {
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection); this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection);

View File

@ -27,15 +27,17 @@ export default class StyleRuleManager extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.callback = callback; this.callback = callback;
this.refreshData = this.refreshData.bind(this);
this.toggleSubscription = this.toggleSubscription.bind(this);
if (suppressSubscriptionOnEdit) { if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this)); this.openmct.editor.on('isEditing', this.toggleSubscription);
this.isEditing = this.openmct.editor.editing; this.isEditing = this.openmct.editor.editing;
} }
if (styleConfiguration) { if (styleConfiguration) {
this.initialize(styleConfiguration); this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) { if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData.bind(this)); this.openmct.time.on("bounds", this.refreshData);
this.subscribeToConditionSet(); this.subscribeToConditionSet();
} else { } else {
this.applyStaticStyle(); this.applyStaticStyle();

View File

@ -215,7 +215,8 @@ export default {
}, },
isEditing: { isEditing: {
type: Boolean, type: Boolean,
required: true required: true,
default: false
}, },
telemetry: { telemetry: {
type: Array, type: Array,

View File

@ -20,12 +20,49 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import AlphanumericFormat from './components/AlphanumericFormat.vue';
'./components/AlphanumericFormatView.vue',
'vue'
], function (AlphanumericFormatView, Vue) {
function AlphanumericFormatViewProvider(openmct, options) { import Vue from 'vue';
class AlphanumericFormatView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
name: 'AlphanumericFormat',
components: {
AlphanumericFormat
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
template: '<alphanumeric-format ref="alphanumericFormat"></alphanumeric-format>'
});
}
getViewContext() {
if (this.component) {
return {};
}
return this.component.$refs.alphanumericFormat.getViewContext();
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
export default function AlphanumericFormatViewProvider(openmct, options) {
function isTelemetryObject(selectionPath) { function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item; let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item; let parentObject = selectionPath[1].context.item;
@ -51,40 +88,10 @@ define([
return selection.every(isTelemetryObject); return selection.every(isTelemetryObject);
}, },
view: function (domainObject, objectPath) { view: function (domainObject, objectPath) {
let component; return new AlphanumericFormatView(openmct, domainObject, objectPath);
return {
show: function (element) {
component = new Vue({
el: element,
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
provide: {
openmct,
objectPath
},
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
});
},
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}, },
priority: function () { priority: function () {
return 1; return 1;
} }
}; };
} }
return AlphanumericFormatViewProvider;
});

View File

@ -14,7 +14,7 @@ export default class CopyToClipboardAction {
invoke(objectPath, view = {}) { invoke(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext(); const viewContext = view.getViewContext && view.getViewContext();
const formattedValue = viewContext.formattedValueForCopy(); const formattedValue = viewContext.row.formattedValueForCopy();
clipboard.updateClipboard(formattedValue) clipboard.updateClipboard(formattedValue)
.then(() => { .then(() => {
@ -26,9 +26,13 @@ export default class CopyToClipboardAction {
} }
appliesTo(objectPath, view = {}) { appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext(); const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return false;
}
return viewContext && viewContext.formattedValueForCopy return row.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function'; && typeof row.formattedValueForCopy === 'function';
} }
} }

View File

@ -52,7 +52,8 @@
<script> <script>
export default { export default {
inject: ['openmct'], name: 'AlphanumericFormat',
inject: ['openmct', 'objectPath'],
data() { data() {
return { return {
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),

View File

@ -56,6 +56,7 @@
:index="index" :index="index"
:multi-select="selectedLayoutItems.length > 1" :multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing" :is-editing="isEditing"
@contextClick="updateViewContext"
@move="move" @move="move"
@endMove="endMove" @endMove="endMove"
@endLineResize="endLineResize" @endLineResize="endLineResize"
@ -140,7 +141,7 @@ function getItemDefinition(itemType, ...options) {
export default { export default {
components: components, components: components,
inject: ['openmct', 'options', 'objectPath'], inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -155,7 +156,8 @@ export default {
return { return {
initSelectIndex: undefined, initSelectIndex: undefined,
selection: [], selection: [],
showGrid: true showGrid: true,
viewContext: {}
}; };
}, },
computed: { computed: {
@ -819,6 +821,12 @@ export default {
}, },
toggleGrid() { toggleGrid() {
this.showGrid = !this.showGrid; this.showGrid = !this.showGrid;
},
updateViewContext(viewContext) {
this.viewContext.row = viewContext;
},
getViewContext() {
return this.viewContext;
} }
} }
}; };

View File

@ -102,7 +102,7 @@ export default {
LayoutFrame LayoutFrame
}, },
mixins: [conditionalStylesMixin], mixins: [conditionalStylesMixin],
inject: ['openmct', 'objectPath'], inject: ['openmct', 'objectPath', 'currentView'],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -294,16 +294,6 @@ export default {
this.requestHistoricalData(this.domainObject); this.requestHistoricalData(this.domainObject);
} }
}, },
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
};
}
};
},
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.mutablePromise = undefined; this.mutablePromise = undefined;
@ -338,30 +328,38 @@ export default {
this.$emit('formatChanged', this.item, format); this.$emit('formatChanged', this.item, format);
}, },
updateViewContext() {
this.$emit('contextClick', {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
});
},
async getContextMenuActions() { async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook(); const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
const actionsObject = actionCollection.getActionsObject();
let copyToNotebookAction = actionsObject.copyToNotebook;
let defaultNotebookName;
if (defaultNotebook) { if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`; defaultNotebookName = `Copy to Notebook ${defaultPath}`;
} else {
actionsObject.copyToNotebook = undefined;
delete actionsObject.copyToNotebook;
} }
return CONTEXT_MENU_ACTIONS.map(actionKey => { return CONTEXT_MENU_ACTIONS
return actionsObject[actionKey]; .map(actionKey => {
}).filter(action => action !== undefined); const action = this.openmct.actions.getAction(actionKey);
if (action.key === 'copyToNotebook') {
action.name = defaultNotebookName;
}
return action;
})
.filter(action => action.name !== undefined);
}, },
async showContextMenu(event) { async showContextMenu(event) {
this.updateViewContext();
const contextMenuActions = await this.getContextMenuActions(); const contextMenuActions = await this.getContextMenuActions();
const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView);
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); this.openmct.menus.showMenu(event.x, event.y, menuItems);
}, },
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;

View File

@ -21,6 +21,10 @@
margin-left: $interiorMargin; margin-left: $interiorMargin;
} }
&__value {
@include isLimit();
}
.c-frame & { .c-frame & {
@include abs(); @include abs();
border: 1px solid transparent; border: 1px solid transparent;

View File

@ -20,13 +20,81 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import Layout from './components/DisplayLayout.vue';
import Vue from 'vue';
import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction'; import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import objectUtils from 'objectUtils';
import Vue from 'vue';
class DisplayLayoutView {
constructor(openmct, domainObject, objectPath, options) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.options = options;
this.component = undefined;
}
show(container, isEditing) {
this.component = new Vue({
el: container,
components: {
DisplayLayout
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
options: this.options,
objectUtils,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
isEditing
};
},
template: '<display-layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></display-layout>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.displayLayout.getViewContext();
}
getSelectionContext() {
return {
item: this.domainObject,
supportsMultiSelect: true,
addElement: this.component && this.component.$refs.displayLayout.addElement,
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
};
}
onEditModeChange(isEditing) {
this.component.isEditing = isEditing;
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
export default function DisplayLayoutPlugin(options) { export default function DisplayLayoutPlugin(options) {
return function (openmct) { return function (openmct) {
@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) {
return domainObject.type === 'layout'; return domainObject.type === 'layout';
}, },
view: function (domainObject, objectPath) { view: function (domainObject, objectPath) {
let component; return new DisplayLayoutView(openmct, domainObject, objectPath, options);
return {
show(container) {
component = new Vue({
el: container,
components: {
Layout
},
provide: {
openmct,
objectUtils,
options,
objectPath
},
data() {
return {
domainObject: domainObject,
isEditing: openmct.editor.isEditing()
};
},
template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
});
},
getSelectionContext() {
return {
item: domainObject,
supportsMultiSelect: true,
addElement: component && component.$refs.displayLayout.addElement,
removeItem: component && component.$refs.displayLayout.removeItem,
orderItem: component && component.$refs.displayLayout.orderItem,
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: component && component.$refs.displayLayout.toggleGrid
};
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy() {
component.$destroy();
}
};
}, },
priority() { priority() {
return 100; return 100;

View File

@ -37,7 +37,15 @@ export default class DuplicateAction {
let duplicationTask = new DuplicateTask(this.openmct); let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0]; let originalObject = objectPath[0];
let parent = objectPath[1]; let parent = objectPath[1];
let userInput = await this.getUserInput(originalObject, parent); let userInput;
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let newParent = userInput.location; let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject); let inNavigationPath = this.inNavigationPath(originalObject);
@ -71,7 +79,8 @@ export default class DuplicateAction {
updateNameCheck(object, name) { updateNameCheck(object, name) {
if (object.name !== name) { if (object.name !== name) {
this.openmct.objects.mutate(object, 'name', name); object.name = name;
this.openmct.objects.save(object);
} }
} }
@ -95,7 +104,7 @@ export default class DuplicateAction {
cssClass: "l-input-lg" cssClass: "l-input-lg"
}, },
{ {
name: "location", name: "Location",
cssClass: "grows", cssClass: "grows",
control: "locator", control: "locator",
validate: this.validate(object, parent), validate: this.validate(object, parent),

View File

@ -121,10 +121,9 @@ describe("The Duplicate Action plugin", () => {
describe("when moving an object to a new parent", () => { describe("when moving an object to a new parent", () => {
beforeEach(async (done) => { beforeEach(async () => {
duplicateTask = new DuplicateTask(openmct); duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject); await duplicateTask.duplicate(parentObject, anotherParentObject);
done();
}); });
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => { it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
@ -143,15 +142,15 @@ describe("The Duplicate Action plugin", () => {
}); });
describe("when a new name is provided for the duplicated object", () => { describe("when a new name is provided for the duplicated object", () => {
it("the name is updated", () => {
const NEW_NAME = 'New Name'; const NEW_NAME = 'New Name';
let childName;
beforeEach(() => {
duplicateTask = new DuplicateAction(openmct); duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME); duplicateTask.updateNameCheck(parentObject, NEW_NAME);
});
it("the name is updated", () => { childName = parentObject.name;
let childName = parentObject.name;
expect(childName).toEqual(NEW_NAME); expect(childName).toEqual(NEW_NAME);
}); });
}); });

View File

@ -23,6 +23,11 @@
body.mobile & { body.mobile & {
flex: 1 0 auto; flex: 1 0 auto;
} }
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
} }
/******************************* GRID ITEMS */ /******************************* GRID ITEMS */

View File

@ -22,4 +22,9 @@
@include isAlias(); @include isAlias();
} }
} }
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
} }

View File

@ -24,10 +24,15 @@ import {
resetApplicationState resetApplicationState
} from 'utils/testing'; } from 'utils/testing';
describe("the plugin", () => { describe("the goToOriginalAction plugin", () => {
let openmct; let openmct;
let goToFolderAction; let goToOriginalAction;
let mockRootFolder;
let mockSubFolder;
let mockSubSubFolder;
let mockObject;
let mockObjectPath; let mockObjectPath;
let hash;
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -35,7 +40,7 @@ describe("the plugin", () => {
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
goToFolderAction = openmct.actions._allActions.goToOriginal; goToOriginalAction = openmct.actions._allActions.goToOriginal;
}); });
afterEach(() => { afterEach(() => {
@ -43,34 +48,153 @@ describe("the plugin", () => {
}); });
it('installs the go to folder action', () => { it('installs the go to folder action', () => {
expect(goToFolderAction).toBeDefined(); expect(goToOriginalAction).toBeDefined();
}); });
describe('when invoked', () => { describe('when invoked', () => {
beforeEach(() => { beforeEach(() => {
mockObjectPath = [{ mockRootFolder = getMockObject('mock-root');
name: 'mock folder', mockSubFolder = getMockObject('mock-sub');
type: 'folder', mockSubSubFolder = getMockObject('mock-sub-sub');
identifier: { mockObject = getMockObject('mock-table');
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath); mockObjectPath = [
mockObject,
mockSubSubFolder,
mockSubFolder,
mockRootFolder
];
spyOn(openmct.objects, 'get').and.callFake(identifier => {
const mockedObject = getMockObject(identifier);
return Promise.resolve(mockedObject);
}); });
it('goes to the original location', (done) => { spyOn(openmct.router, 'navigate').and.callFake(navigateTo => {
setTimeout(() => { hash = navigateTo;
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); });
done();
}, 1500); return goToOriginalAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
const originalLocationHash = '#/browse/mock-root/mock-table';
return waitForNavigation(() => {
return hash === originalLocationHash;
}).then(() => {
expect(hash).toEqual(originalLocationHash);
}); });
}); });
});
function waitForNavigation(navigated) {
return new Promise((resolve, reject) => {
const start = Date.now();
checkNavigated();
function checkNavigated() {
const elapsed = Date.now() - start;
if (navigated()) {
resolve();
} else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) {
reject("didn't navigate in time");
} else {
setTimeout(checkNavigated);
}
}
});
}
function getMockObject(key) {
const id = typeof key === 'string' ? key : key.key;
const mockMCTObjects = {
"ROOT": {
"composition": [
{
"namespace": "",
"key": "mock-root"
}
],
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-root": {
"composition": [
{
"namespace": "",
"key": "mock-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "root",
"type": "folder",
"id": "mock-root",
"location": "ROOT",
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-sub": {
"composition": [
{
"namespace": "",
"key": "mock-sub-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub",
"type": "folder",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-sub"
}
},
"mock-table": {
"composition": [],
"configuration": {
"columnWidths": {},
"hiddenColumns": {}
},
"name": "table",
"type": "table",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-table"
}
},
"mock-sub-sub": {
"composition": [
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub sub",
"type": "folder",
"location": "mock-sub",
"identifier": {
"namespace": "",
"key": "mock-sub-sub"
}
}
};
return mockMCTObjects[id];
}
}); });

View File

@ -0,0 +1,51 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<a class="c-hyperlink"
:class="{
'c-hyperlink--button' : isButton
}"
:target="domainObject.linkTarget"
:href="domainObject.url"
>
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
</a>
</template>
<script>
export default {
inject: ['domainObject'],
computed: {
isButton() {
if (this.domainObject.displayFormat === "link") {
return false;
}
return true;
}
}
};
</script>

View File

@ -20,39 +20,40 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/** import HyperlinkLayout from './HyperlinkLayout.vue';
* Module defining NewTabAction (Originally NewWindowAction). Created by vwoeltje on 11/18/14. import Vue from 'vue';
*/
define(
[],
function () {
/**
* The new tab action allows a domain object to be opened
* into a new browser tab.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function NewTabAction(urlService, $window, context) {
context = context || {};
this.urlService = urlService; export default function HyperlinkProvider(openmct) {
this.open = function () {
arguments[0] += "&hideTree=true&hideInspector=true";
$window.open.apply($window, arguments);
};
// Choose the object to be opened into a new tab return {
this.domainObject = context.selectedObject || context.domainObject; key: 'hyperlink.view',
name: 'Hyperlink',
cssClass: 'icon-chain-links',
canView(domainObject) {
return domainObject.type === 'hyperlink';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
HyperlinkLayout
},
provide: {
domainObject
},
template: '<hyperlink-layout></hyperlink-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
} }
NewTabAction.prototype.perform = function () {
this.open(
this.urlService.urlForNewTab("browse", this.domainObject),
"_blank"
);
}; };
return NewTabAction;
} }
); };
}

View File

@ -0,0 +1,89 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import HyperlinkProvider from './HyperlinkProvider';
export default function () {
return function install(openmct) {
openmct.types.addType('hyperlink', {
name: 'Hyperlink',
key: 'hyperlink',
description: 'A hyperlink to redirect to a different link',
creatable: true,
cssClass: 'icon-chain-links',
initialize: function (domainObject) {
domainObject.displayFormat = "link";
domainObject.linkTarget = "_self";
},
form: [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"name": "Button",
"value": "button"
}
],
"cssClass": "l-inline"
},
{
"key": "linkTarget",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "_self"
},
{
"name": "Open in a new tab",
"value": "_blank"
}
],
"cssClass": "l-inline"
}
]
});
openmct.objectViews.addProvider(new HyperlinkProvider(openmct));
};
}

View File

@ -0,0 +1,130 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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 HyperlinkPlugin from "./plugin";
function getView(openmct, domainObj, objectPath) {
const applicableViews = openmct.objectViews.get(domainObj, objectPath);
const hyperLinkView = applicableViews.find((viewProvider) => viewProvider.key === 'hyperlink.view');
return hyperLinkView.view(domainObj);
}
function destroyView(view) {
return view.destroy();
}
describe("The controller for hyperlinks", function () {
let mockDomainObject;
let mockObjectPath;
let openmct;
let element;
let child;
let view;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock hyperlink',
type: 'hyperlink',
identifier: {
key: 'mock-hyperlink',
namespace: ''
}
}
];
mockDomainObject = {
displayFormat: "",
linkTarget: "",
name: "Unnamed HyperLink",
type: "hyperlink",
location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
modified: 1627483839783,
url: "123",
displayText: "123",
persisted: 1627483839783,
id: "3d9c243d-dffb-446b-8474-d9931a99d679",
identifier: {
namespace: "",
key: "3d9c243d-dffb-446b-8474-d9931a99d679"
}
};
openmct = createOpenMct();
openmct.install(new HyperlinkPlugin());
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
destroyView(view);
return resetApplicationState(openmct);
});
it("knows when it should open a new tab", () => {
mockDomainObject.displayFormat = "link";
mockDomainObject.linkTarget = "_blank";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink').target).toBe('_blank');
});
it("knows when it should open in the same tab", function () {
mockDomainObject.displayFormat = "button";
mockDomainObject.linkTarget = "_self";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink').target).toBe('_self');
});
it("knows when it is a button", function () {
mockDomainObject.displayFormat = "button";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink--button')).toBeDefined();
});
it("knows when it is a link", function () {
mockDomainObject.displayFormat = "link";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button');
});
});

View File

@ -0,0 +1,37 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
export default class ImageryView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
_getInstance() {
return this.component;
}
}

View File

@ -19,9 +19,7 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import ImageryView from './ImageryView';
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
export default function ImageryViewProvider(openmct) { export default function ImageryViewProvider(openmct) {
const type = 'example.imagery'; const type = 'example.imagery';
@ -42,28 +40,8 @@ export default function ImageryViewProvider(openmct) {
canView: function (domainObject) { canView: function (domainObject) {
return hasImageTelemetry(domainObject); return hasImageTelemetry(domainObject);
}, },
view: function (domainObject) { view: function (domainObject, objectPath) {
let component; return new ImageryView(openmct, domainObject, objectPath);
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct,
domainObject
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
} }
}; };
} }

View File

@ -23,7 +23,7 @@
<template> <template>
<div <div
class="c-compass" class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`" :style="`width: 100%; height: 100%`"
> >
<CompassHUD <CompassHUD
v-if="hasCameraFieldOfView" v-if="hasCameraFieldOfView"
@ -33,13 +33,12 @@
/> />
<CompassRose <CompassRose
v-if="hasCameraFieldOfView" v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView" :camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan" :camera-pan="cameraPan"
:lock-compass="lockCompass" :compass-rose-sizing-classes="compassRoseSizingClasses"
@toggle-lock-compass="toggleLockCompass" :heading="heading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
/> />
</div> </div>
</template> </template>
@ -56,42 +55,20 @@ export default {
CompassRose CompassRose
}, },
props: { props: {
containerWidth: { compassRoseSizingClasses: {
type: Number, type: String,
required: true
},
containerHeight: {
type: Number,
required: true
},
naturalAspectRatio: {
type: Number,
required: true required: true
}, },
image: { image: {
type: Object, type: Object,
required: true required: true
}, },
lockCompass: { sizedImageDimensions: {
type: Boolean, type: Object,
required: true required: true
} }
}, },
computed: { computed: {
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
sizedImageDimensions.height = this.containerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.containerWidth;
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
}
return sizedImageDimensions;
},
hasCameraFieldOfView() { hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0; return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
}, },

View File

@ -21,152 +21,203 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div ref="compassRoseWrapper"
class="w-direction-rose" class="w-direction-rose"
:class="compassRoseSizingClasses" :class="compassRoseSizingClasses"
>
<div
class="c-direction-rose"
@click="toggleLockCompass" @click="toggleLockCompass"
> >
<div <svg ref="compassRoseSvg"
class="c-nsew" class="c-compass-rose-svg"
:style="compassRoseStyle"
>
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100" viewBox="0 0 100 100"
> >
<rect <mask id="mask0"
class="c-nsew__tick c-tick-ne" mask-type="alpha"
x="49" maskUnits="userSpaceOnUse"
x="0"
y="0" y="0"
width="2" width="100"
height="5" height="100"
/>
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-sw"
x="49"
y="95"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-nw"
x="0"
y="49"
width="5"
height="2"
/>
</svg>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
> >
<polygon <circle cx="50"
class="c-nsew__tick c-tick-n" cy="50"
points="50,0 60,10 40,10" r="50"
fill="black"
/> />
<rect </mask>
class="c-nsew__tick c-tick-e" <g class="c-cr__compass-wrapper">
x="95" <g class="c-cr__compass-main"
y="49" mask="url(#mask0)"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
/>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
> >
</div> <!-- Background and clipped elements -->
<rect class="c-cr__bg"
<div width="100"
v-if="hasSunHeading" height="100"
class="c-sun" fill="black"
/>
<rect class="c-cr__edge"
width="100"
height="100"
fill="url(#paint0_radial)"
/>
<rect v-if="hasSunHeading"
class="c-cr__sun"
width="100"
height="100"
fill="url(#paint1_radial)"
:style="sunHeadingStyle" :style="sunHeadingStyle"
></div> />
<div <!-- Camera FOV -->
class="c-cam-field" <mask id="mask2"
class="c-cr__cam-fov-l-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="50"
height="100"
>
<rect width="51"
height="100"
/>
</mask>
<mask id="mask1"
class="c-cr__cam-fov-r-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="50"
y="0"
width="50"
height="100"
>
<rect x="49"
width="51"
height="100"
/>
</mask>
<g class="c-cr__cam-fov"
:style="cameraPanStyle" :style="cameraPanStyle"
> >
<div class="cam-field-half cam-field-half-l"> <g mask="url(#mask2)">
<div <rect class="c-cr__cam-fov-r"
class="cam-field-area" x="49"
:style="cameraFOVStyleLeftHalf" width="51"
></div> height="100"
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf" :style="cameraFOVStyleRightHalf"
></div> />
</div> </g>
</div> <g mask="url(#mask1)">
</div> <rect class="c-cr__cam-fov-l"
width="51"
height="100"
:style="cameraFOVStyleLeftHalf"
/>
</g>
</g>
</g>
<!-- Spacecraft body -->
<path v-if="hasHeading"
class="c-cr__spacecraft-body"
fill-rule="evenodd"
clip-rule="evenodd"
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
:style="headingStyle"
/>
<!-- NSEW and ticks -->
<g class="c-cr__nsew"
:style="compassRoseStyle"
>
<g class="c-cr__ticks-major">
<path d="M50 3L43 10H57L50 3Z" />
<path d="M4 51V49H10V51H4Z"
class="--hide-min"
/>
<path d="M49 96V90H51V96H49Z"
class="--hide-min"
/>
<path d="M90 49V51H96V49H90Z"
class="--hide-min"
/>
</g>
<g class="c-cr__ticks-minor --hide-small">
<path d="M4 51V49H10V51H4Z" />
<path d="M90 51V49H96V51H90Z" />
<path d="M51 96H49V90H51V96Z" />
<path d="M51 10L49 10V4L51 4V10Z" />
</g>
<g class="c-cr__nsew-text">
<path :style="cardinalTextRotateW"
class="c-cr__nsew-w --hide-small"
d="M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z"
/>
<path :style="cardinalTextRotateE"
class="c-cr__nsew-e --hide-small"
d="M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z"
/>
<path :style="cardinalTextRotateS"
class="c-cr__nsew-s --hide-small"
d="M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z"
/>
<path :style="cardinalTextRotateN"
class="c-cr__nsew-n"
d="M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z"
/>
</g>
</g>
</g>
<defs>
<radialGradient id="paint0_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 50) rotate(90) scale(50)"
>
<stop offset="0.751387"
stop-opacity="0"
/>
<stop offset="1"
stop-color="white"
/>
</radialGradient>
<radialGradient id="paint1_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
>
<stop offset="0.716377"
stop-color="#FFCC00"
/>
<stop offset="1"
stop-color="#FF9900"
stop-opacity="0"
/>
</radialGradient>
</defs>
</svg>
</div> </div>
</template> </template>
<script> <script>
import { rotate } from './utils'; import { rotate } from './utils';
import { throttle } from 'lodash';
export default { export default {
props: { props: {
sizedImageWidth: { compassRoseSizingClasses: {
type: Number, type: String,
required: true required: true
}, },
heading: { heading: {
type: Number, type: Number,
required: true required: true,
default() {
return 0;
}
}, },
sunHeading: { sunHeading: {
type: Number, type: Number,
@ -178,58 +229,39 @@ export default {
}, },
cameraPan: { cameraPan: {
type: Number, type: Number,
required: true required: true,
default() {
return 0;
}
}, },
lockCompass: { sizedImageDimensions: {
type: Boolean, type: Object,
required: true required: true
} }
}, },
data() {
return {
lockCompass: true
};
},
computed: { computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
compassRoseStyle() { compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` }; return { transform: `rotate(${ this.north }deg)` };
}, },
north() { north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0; return this.lockCompass ? rotate(-this.cameraPan) : 0;
}, },
northTextTransform() { cardinalTextRotateN() {
return this.cardinalPointsTextTransform.north; return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
}, },
eastTextTransform() { cardinalTextRotateS() {
return this.cardinalPointsTextTransform.east; return { transform: `translateY(30%) rotate(${ -this.north }deg)` };
}, },
southTextTransform() { cardinalTextRotateE() {
return this.cardinalPointsTextTransform.south; return { transform: `translateX(30%) rotate(${ -this.north }deg)` };
}, },
westTextTransform() { cardinalTextRotateW() {
return this.cardinalPointsTextTransform.west; return { transform: `translateX(-30%) rotate(${ -this.north }deg)` };
},
cardinalPointsTextTransform() {
/**
* cardinal points text must be rotated
* in the opposite direction that north is rotated
* to keep text vertically oriented
*/
const rotation = `rotate(${ -this.north })`;
return {
north: `translate(50,23) ${ rotation }`,
east: `translate(82,50) ${ rotation }`,
south: `translate(18,50) ${ rotation }`,
west: `translate(50,82) ${ rotation }`
};
}, },
hasHeading() { hasHeading() {
return this.heading !== undefined; return this.heading !== undefined;
@ -238,7 +270,7 @@ export default {
const rotation = rotate(this.north, this.heading); const rotation = rotate(this.north, this.heading);
return { return {
transform: `translateX(-50%) rotate(${ rotation }deg)` transform: `rotate(${ rotation }deg)`
}; };
}, },
hasSunHeading() { hasSunHeading() {
@ -262,20 +294,37 @@ export default {
// rotated counter-clockwise from camera pan angle // rotated counter-clockwise from camera pan angle
cameraFOVStyleLeftHalf() { cameraFOVStyleLeftHalf() {
return { return {
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)` transform: `rotate(${ this.cameraAngleOfView / 2 }deg)`
}; };
}, },
// right half of camera field of view // right half of camera field of view
// rotated clockwise from camera pan angle // rotated clockwise from camera pan angle
cameraFOVStyleRightHalf() { cameraFOVStyleRightHalf() {
return { return {
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)` transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
}; };
} }
}, },
watch: {
sizedImageDimensions() {
this.debounceResizeSvg();
}
},
mounted() {
this.debounceResizeSvg = throttle(this.resizeSvg, 100);
this.$nextTick(() => {
this.debounceResizeSvg();
});
},
methods: { methods: {
resizeSvg() {
const svg = this.$refs.compassRoseSvg;
svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth);
svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight);
},
toggleLockCompass() { toggleLockCompass() {
this.$emit('toggle-lock-compass'); this.lockCompass = !this.lockCompass;
} }
} }
}; };

View File

@ -12,9 +12,8 @@ $elemBg: rgba(black, 0.7);
.c-compass { .c-compass {
pointer-events: none; // This allows the image element to receive a browser-level context click pointer-events: none; // This allows the image element to receive a browser-level context click
position: absolute; position: absolute;
left: 50%; left: 0;
top: 50%; top: 0;
transform: translate(-50%, -50%);
z-index: 1; z-index: 1;
@include userSelectNone; @include userSelectNone;
} }
@ -81,114 +80,55 @@ $elemBg: rgba(black, 0.7);
transform: translateX(-50%); transform: translateX(-50%);
z-index: 1; z-index: 1;
} }
} }
/***************************** COMPASS DIRECTIONS */ /***************************** COMPASS SVG */
.c-nsew { .c-compass-rose-svg {
$color: $interfaceKeyColor; $color: $interfaceKeyColor;
$inset: 5%; position: absolute;
$tickHeightPerc: 15%; top: 0; left: 0;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
&__tick, g, path, rect {
&__label { // In an SVG, rotation occurs about the center of the SVG, not the element
transform-origin: center;
}
.c-cr {
&__bg {
fill: #000;
opacity: 0.8;
}
&__edge {
opacity: 0.1;
}
&__sun {
opacity: 0.7;
}
&__cam-fov-l,
&__cam-fov-r {
// Cam FOV indication
opacity: 0.2;
fill: #fff;
}
&__nsew-text,
&__spacecraft-body,
&__ticks-major,
&__ticks-minor {
fill: $color; fill: $color;
} }
&__minor-ticks { &__ticks-minor {
opacity: 0.5; opacity: 0.5;
transform-origin: center;
transform: rotate(45deg); transform: rotate(45deg);
} }
&__label { &__spacecraft-body {
dominant-baseline: central;
font-size: 1.25em;
font-weight: bold;
}
.c-label-n {
font-size: 2em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.3; opacity: 0.3;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
} }
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
}
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s;
width: $s;
left: 50%;
top: 50%;
opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
} }
} }
@ -196,32 +136,28 @@ $elemBg: rgba(black, 0.7);
.w-direction-rose { .w-direction-rose {
$s: 10%; $s: 10%;
$m: 2%; $m: 2%;
cursor: pointer;
pointer-events: all;
position: absolute; position: absolute;
bottom: $m; bottom: $m;
left: $m; left: $m;
width: $s; width: $s;
padding-top: $s; padding-top: $s;
z-index: 2;
&.--rose-min { &.--rose-min {
$s: 30px; $s: 30px;
width: $s; width: $s;
padding-top: $s; padding-top: $s;
.--hide-min {
display: none;
}
} }
&.--rose-small { &.--rose-small {
.c-nsew__minor-ticks, .--hide-small {
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
display: none; display: none;
} }
.c-label-n {
font-size: 2.5em;
}
} }
&.--rose-max { &.--rose-max {
@ -230,44 +166,3 @@ $elemBg: rgba(black, 0.7);
padding-top: $s; padding-top: $s;
} }
} }
.c-direction-rose {
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
transform-origin: 0 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
pointer-events: all;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0;
left: 50%;
height: $s;
width: $s;
transform: translate(-50%, -60%);
}
}
}

View File

@ -55,11 +55,18 @@
></a> ></a>
</span> </span>
</div> </div>
<div class="c-imagery__main-image__bg" <div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }" :class="{'paused unnsynced': isPaused,'stale':false }"
@click="expand"
> >
<img <div class="image-wrapper"
ref="focusedImage" :style="{
'width': `${sizedImageDimensions.width}px`,
'height': `${sizedImageDimensions.height}px`
}"
>
<img ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image" class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl" :src="imageUrl"
:style="{ :style="{
@ -70,14 +77,13 @@
> >
<Compass <Compass
v-if="shouldDisplayCompass" v-if="shouldDisplayCompass"
:container-width="imageContainerWidth" :compass-rose-sizing-classes="compassRoseSizingClasses"
:container-height="imageContainerHeight"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:image="focusedImage" :image="focusedImage"
:lock-compass="lockCompass" :natural-aspect-ratio="focusedImageNaturalAspectRatio"
@toggle-lock-compass="toggleLockCompass" :sized-image-dimensions="sizedImageDimensions"
/> />
</div> </div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev" <button class="c-nav c-nav--prev"
title="Previous image" title="Previous image"
@ -124,9 +130,15 @@
</div> </div>
</div> </div>
<div <div
ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper" class="c-imagery__thumbs-wrapper"
:class="{'is-paused': isPaused}" :class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll" @scroll="handleScroll"
> >
<div v-for="(image, index) in imageHistory" <div v-for="(image, index) in imageHistory"
@ -146,14 +158,22 @@
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div> <div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div> </div>
</div> </div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight('reset')"
></button>
</div>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import Compass from './Compass/Compass.vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
@ -171,11 +191,13 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39; const ARROW_RIGHT = 39;
const ARROW_LEFT = 37; const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
export default { export default {
components: { components: {
Compass Compass
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
@ -204,10 +226,23 @@ export default {
focusedImageNaturalAspectRatio: undefined, focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined, imageContainerWidth: undefined,
imageContainerHeight: undefined, imageContainerHeight: undefined,
lockCompass: true lockCompass: true,
resizingWindow: false
}; };
}, },
computed: { computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageDimensions.width < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageDimensions.width > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
time() { time() {
return this.formatTime(this.focusedImage); return this.formatTime(this.focusedImage);
}, },
@ -331,6 +366,20 @@ export default {
} }
return isFresh; return isFresh;
},
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
sizedImageDimensions.height = this.imageContainerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.imageContainerWidth;
sizedImageDimensions.height = this.imageContainerWidth * this.focusedImageNaturalAspectRatio;
}
return sizedImageDimensions;
} }
}, },
watch: { watch: {
@ -379,10 +428,14 @@ export default {
_.debounce(this.resizeImageContainer, 400); _.debounce(this.resizeImageContainer, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer); this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage); this.imageContainerResizeObserver.observe(this.$refs.imageBG);
},
updated() { // For adjusting scroll bar size and position when resizing thumbs wrapper
this.scrollToRight(); this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unsubscribe) { if (this.unsubscribe) {
@ -394,6 +447,10 @@ export default {
this.imageContainerResizeObserver.disconnect(); this.imageContainerResizeObserver.disconnect();
} }
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) { if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy(); this.relatedTelemetry.destroy();
} }
@ -413,6 +470,16 @@ export default {
} }
}, },
methods: { methods: {
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions
&& visibleActions.find(action => action.key === 'large.view');
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.onItemClicked();
}
},
async initializeRelatedTelemetry() { async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry( this.relatedTelemetry = new RelatedTelemetry(
this.openmct, this.openmct,
@ -561,17 +628,15 @@ export default {
}, },
handleScroll() { handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper; const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) { if (!thumbsWrapper || this.resizingWindow) {
return; return;
} }
const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper; const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;
const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
|| (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll; this.autoScroll = !disableScroll;
}, },
paused(state, type) { paused(state, type) {
this.isPaused = state; this.isPaused = state;
if (type === 'button') { if (type === 'button') {
@ -584,6 +649,7 @@ export default {
} }
this.autoScroll = true; this.autoScroll = true;
this.scrollToRight();
}, },
scrollToFocused() { scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper; const thumbsWrapper = this.$refs.thumbsWrapper;
@ -600,8 +666,8 @@ export default {
}); });
} }
}, },
scrollToRight() { scrollToRight(type) {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return; return;
} }
@ -610,7 +676,9 @@ export default {
return; return;
} }
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0); this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
}, },
setFocusedImage(index, thumbnailClick = false) { setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) { if (this.isPaused && !thumbnailClick) {
@ -678,9 +746,9 @@ export default {
image.imageDownloadName = this.getImageDownloadName(datum); image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image); this.imageHistory.push(image);
if (setFocused) { if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1); this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
} }
}, },
getFormatter(key) { getFormatter(key) {
@ -808,16 +876,31 @@ export default {
}, { once: true }); }, { once: true });
}, },
resizeImageContainer() { resizeImageContainer() {
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) { if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.focusedImage.clientWidth; this.imageContainerWidth = this.$refs.imageBG.clientWidth;
} }
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) { if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight; this.imageContainerHeight = this.$refs.imageBG.clientHeight;
} }
}, },
toggleLockCompass() { handleThumbWindowResizeStart() {
this.lockCompass = !this.lockCompass; if (!this.autoScroll) {
return;
}
// To hide resume button while scrolling
this.resizingWindow = true;
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.$nextTick(() => {
this.resizingWindow = false;
});
} }
} }
}; };

View File

@ -1,7 +1,7 @@
.c-imagery { .c-imagery {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; height: 100%;
overflow: hidden; overflow: hidden;
&:focus { &:focus {
@ -22,6 +22,9 @@
&__bg { &__bg {
background-color: $colorPlotBg; background-color: $colorPlotBg;
border: 1px solid transparent; border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto; flex: 1 1 auto;
height: 0; height: 0;
@ -33,7 +36,6 @@
&__image { &__image {
height: 100%; height: 100%;
width: 100%; width: 100%;
object-fit: contain;
} }
} }
@ -93,24 +95,43 @@
} }
&__thumbs-wrapper { &__thumbs-wrapper {
flex: 0 0 auto; display: flex; // Uses row layout
&.is-autoscroll-off {
background: $colorInteriorBorder;
[class*='__auto-scroll-resume-button'] {
display: block;
}
}
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
}
&__thumbs-scroll-area {
flex: 0 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 135px; height: 135px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm; padding-bottom: $interiorMarginSm;
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
.c-thumb:last-child { .c-thumb:last-child {
// Hilite the lastest thumb // Hilite the lastest thumb
background: $colorBodyFg; background: $colorBodyFg;
color: $colorBodyBg; color: $colorBodyBg;
} }
} }
&__auto-scroll-resume-button {
display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off
flex: 0 0 auto;
font-size: 0.8em;
margin: $interiorMarginSm;
}
} }
/*************************************** THUMBS */ /*************************************** THUMBS */
@ -142,7 +163,7 @@
.l-layout, .l-layout,
.c-fl { .c-fl {
.c-imagery__thumbs-wrapper { .c-imagery__thumbs-scroll-area {
// When Imagery is in a layout, hide the thumbs area // When Imagery is in a layout, hide the thumbs area
display: none; display: none;
} }
@ -173,6 +194,10 @@
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
} }
} }
.s-status-taking-snapshot & {
display: none;
}
} }
&__lc { &__lc {
@ -254,6 +279,10 @@
content: $glyph-icon-play; content: $glyph-icon-play;
} }
} }
.s-status-taking-snapshot & {
display: none;
}
} }
.c-imagery__prev-next-buttons { .c-imagery__prev-next-buttons {
@ -268,6 +297,10 @@
.c-nav { .c-nav {
pointer-events: all; pointer-events: all;
} }
.s-status-taking-snapshot & {
display: none;
}
} }
.c-nav { .c-nav {

View File

@ -92,6 +92,7 @@ describe("The Imagery View Layout", () => {
let resolveFunction; let resolveFunction;
let openmct; let openmct;
let appHolder;
let parent; let parent;
let child; let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@ -195,7 +196,7 @@ describe("The Imagery View Layout", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div'); appHolder = document.createElement('div');
appHolder.style.width = '640px'; appHolder.style.width = '640px';
appHolder.style.height = '480px'; appHolder.style.height = '480px';
@ -209,6 +210,8 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div'); child = document.createElement('div');
parent.appendChild(child); parent.appendChild(child);
// document.querySelector('body').append(parent);
spyOn(window, 'ResizeObserver').and.returnValue({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
disconnect() {} disconnect() {}
@ -277,7 +280,7 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
}); });
it("should show the clicked thumbnail as the main image", (done) => { xit("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url; const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { Vue.nextTick(() => {
@ -314,7 +317,7 @@ describe("The Imagery View Layout", () => {
}); });
}); });
it("should navigate via arrow keys", (done) => { xit("should navigate via arrow keys", (done) => {
let keyOpts = { let keyOpts = {
element: parent.querySelector('.c-imagery'), element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft', key: 'ArrowLeft',
@ -362,5 +365,21 @@ describe("The Imagery View Layout", () => {
done(); done();
}); });
}); });
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
});
}); });
}); });

View File

@ -30,10 +30,6 @@ describe('the plugin', function () {
const TEST_NAMESPACE = 'test'; const TEST_NAMESPACE = 'test';
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct)); openmct.install(new InterceptorPlugin(openmct));
@ -46,7 +42,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -55,6 +51,7 @@ describe('the plugin', function () {
describe('the missingObjectInterceptor', () => { describe('the missingObjectInterceptor', () => {
let mockProvider; let mockProvider;
beforeEach(() => { beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [ mockProvider = jasmine.createSpyObj("mock provider", [
"get" "get"
@ -63,27 +60,28 @@ describe('the plugin', function () {
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider); openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
}); });
it('returns missing objects', (done) => { it('returns missing objects', () => {
const identifier = { const identifier = {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: 'hello' key: 'hello'
}; };
openmct.objects.get(identifier).then((testObject) => {
return openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({ expect(testObject).toEqual({
identifier, identifier,
type: 'unknown', type: 'unknown',
name: 'Missing: test:hello' name: 'Missing: test:hello'
}); });
done();
}); });
}); });
it('returns the My items object if not found', (done) => { it('returns the My items object if not found', () => {
const identifier = { const identifier = {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: 'mine' key: 'mine'
}; };
openmct.objects.get(identifier).then((testObject) => {
return openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({ expect(testObject).toEqual({
identifier, identifier,
"name": "My Items", "name": "My Items",
@ -91,7 +89,6 @@ describe('the plugin', function () {
"composition": [], "composition": [],
"location": "ROOT" "location": "ROOT"
}); });
done();
}); });
}); });

View File

@ -37,7 +37,14 @@ export default class MoveAction {
let oldParent = objectPath[1]; let oldParent = objectPath[1];
let dialogService = this.openmct.$injector.get('dialogService'); let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(object, oldParent); let dialogForm = this.getDialogForm(object, oldParent);
let userInput = await dialogService.getUserInput(dialogForm, { name: object.name }); let userInput;
try {
userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
} catch (err) {
// user canceled, most likely
return;
}
// if we need to update name // if we need to update name
if (object.name !== userInput.name) { if (object.name !== userInput.name) {
@ -104,13 +111,13 @@ export default class MoveAction {
{ {
key: "name", key: "name",
control: "textfield", control: "textfield",
name: "Folder Name", name: "Name",
pattern: "\\S+", pattern: "\\S+",
required: true, required: true,
cssClass: "l-input-lg" cssClass: "l-input-lg"
}, },
{ {
name: "location", name: "Location",
control: "locator", control: "locator",
validate: this.validate(object, parent), validate: this.validate(object, parent),
key: 'location' key: 'location'

View File

@ -19,8 +19,6 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import MoveActionPlugin from './plugin.js';
import MoveAction from './MoveAction.js';
import { import {
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
@ -37,10 +35,6 @@ describe("The Move Action plugin", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
childObject = getMockObjects({ childObject = getMockObjects({
@ -73,11 +67,10 @@ describe("The Move Action plugin", () => {
} }
}).folder; }).folder;
// already installed by default, but never hurts, just adds to context menu
openmct.install(MoveActionPlugin());
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
moveAction = openmct.actions._allActions.move;
}); });
afterEach(() => { afterEach(() => {
@ -85,13 +78,12 @@ describe("The Move Action plugin", () => {
}); });
it("should be defined", () => { it("should be defined", () => {
expect(MoveActionPlugin).toBeDefined(); expect(moveAction).toBeDefined();
}); });
describe("when moving an object to a new parent and removing from the old parent", () => { describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach(() => { beforeEach(() => {
moveAction = new MoveAction(openmct);
moveAction.addToNewParent(childObject, anotherParentObject); moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject); moveAction.removeFromOldParent(parentObject, childObject);
}); });

View File

@ -79,7 +79,7 @@ describe("the plugin", () => {
spyOn(compositionAPI, 'get').and.returnValue(mockComposition); spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
newFolderAction.invoke(mockObjectPath); return newFolderAction.invoke(mockObjectPath);
}); });
it('gets user input for folder name', () => { it('gets user input for folder name', () => {

View File

@ -25,16 +25,20 @@ export default class CopyToNotebookAction {
}); });
} }
invoke(objectPath, view = {}) { invoke(objectPath, view) {
let viewContext = view.getViewContext && view.getViewContext(); const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy;
this.copyToNotebook(viewContext.formattedValueForCopy()); this.copyToNotebook(formattedValueForCopy());
} }
appliesTo(objectPath, view = {}) { appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext(); const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return;
}
return viewContext && viewContext.formattedValueForCopy return row.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function'; && typeof row.formattedValueForCopy === 'function';
} }
} }

View File

@ -31,7 +31,7 @@
</div> </div>
<SearchResults v-if="search.length" <SearchResults v-if="search.length"
ref="searchResults" ref="searchResults"
:domain-object="internalDomainObject" :domain-object="domainObject"
:results="searchResults" :results="searchResults"
@changeSectionPage="changeSelectedSection" @changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries" @updateEntries="updateEntries"
@ -43,15 +43,18 @@
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left" class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]" :class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId" :default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:default-section-id="defaultSectionId" :default-section-id="defaultSectionId"
:domain-object="internalDomainObject" :selected-section-id="selectedSectionId"
:page-title="internalDomainObject.configuration.pageTitle" :domain-object="domainObject"
:section-title="internalDomainObject.configuration.sectionTitle" :page-title="domainObject.configuration.pageTitle"
:section-title="domainObject.configuration.sectionTitle"
:sections="sections" :sections="sections"
:selected-section="selectedSection"
:sidebar-covers-entries="sidebarCoversEntries" :sidebar-covers-entries="sidebarCoversEntries"
@pagesChanged="pagesChanged" @pagesChanged="pagesChanged"
@selectPage="selectPage"
@sectionsChanged="sectionsChanged" @sectionsChanged="sectionsChanged"
@selectSection="selectSection"
@toggleNav="toggleNav" @toggleNav="toggleNav"
/> />
<div class="c-notebook__page-view"> <div class="c-notebook__page-view">
@ -61,10 +64,10 @@
></button> ></button>
<div class="c-notebook__page-view__path c-path"> <div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item"> <span class="c-notebook__path__section c-path__item">
{{ getSelectedSection() ? getSelectedSection().name : '' }} {{ selectedSection ? selectedSection.name : '' }}
</span> </span>
<span class="c-notebook__path__page c-path__item"> <span class="c-notebook__path__page c-path__item">
{{ getSelectedPage() ? getSelectedPage().name : '' }} {{ selectedPage ? selectedPage.name : '' }}
</span> </span>
</div> </div>
<div class="c-notebook__page-view__controls"> <div class="c-notebook__page-view__controls">
@ -115,9 +118,9 @@
<NotebookEntry v-for="entry in filteredAndSortedEntries" <NotebookEntry v-for="entry in filteredAndSortedEntries"
:key="entry.id" :key="entry.id"
:entry="entry" :entry="entry"
:domain-object="internalDomainObject" :domain-object="domainObject"
:selected-page="getSelectedPage()" :selected-page="selectedPage"
:selected-section="getSelectedSection()" :selected-section="selectedSection"
:read-only="false" :read-only="false"
@deleteEntry="deleteEntry" @deleteEntry="deleteEntry"
@updateEntry="updateEntry" @updateEntry="updateEntry"
@ -152,14 +155,19 @@ export default {
SearchResults, SearchResults,
Sidebar Sidebar
}, },
inject: ['openmct', 'domainObject', 'snapshotContainer'], inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
required: true
}
},
data() { data() {
return { return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '', selectedSectionId: this.getDefaultSectionId(),
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '', selectedPageId: this.getDefaultPageId(),
defaultSort: this.domainObject.configuration.defaultSort, defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null, focusEntryId: null,
internalDomainObject: this.domainObject,
search: '', search: '',
searchResults: [], searchResults: [],
showTime: 0, showTime: 0,
@ -168,9 +176,15 @@ export default {
}; };
}, },
computed: { computed: {
defaultPageId() {
return this.getDefaultPageId();
},
defaultSectionId() {
return this.getDefaultSectionId();
},
filteredAndSortedEntries() { filteredAndSortedEntries() {
const filterTime = Date.now(); const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || []; const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
const hours = parseInt(this.showTime, 10); const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours const filteredPageEntriesByTime = hours
@ -185,22 +199,28 @@ export default {
return this.getPages() || []; return this.getPages() || [];
}, },
sections() { sections() {
return this.internalDomainObject.configuration.sections || []; return this.getSections();
}, },
selectedPage() { selectedPage() {
const pages = this.getPages(); const pages = this.getPages();
if (!pages) { const selectedPage = pages.find(page => page.id === this.selectedPageId);
return {};
if (selectedPage) {
return selectedPage;
} }
return pages.find(page => page.isSelected); if (!selectedPage && !pages.length) {
return undefined;
}
return pages[0];
}, },
selectedSection() { selectedSection() {
if (!this.sections.length) { if (!this.sections.length) {
return {}; return null;
} }
return this.sections.find(section => section.isSelected); return this.sections.find(section => section.id === this.selectedSectionId);
} }
}, },
watch: { watch: {
@ -210,16 +230,14 @@ export default {
}, },
beforeMount() { beforeMount() {
this.getSearchResults = debounce(this.getSearchResults, 500); this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
}, },
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar(); this.formatSidebar();
this.setSectionAndPageFromUrl();
window.addEventListener('orientationchange', this.formatSidebar); window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener("hashchange", this.navigateToSectionPage, false); window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
}, },
beforeDestroy() { beforeDestroy() {
if (this.unlisten) { if (this.unlisten) {
@ -227,8 +245,7 @@ export default {
} }
window.removeEventListener('orientationchange', this.formatSidebar); window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener("hashchange", this.navigateToSectionPage); window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
this.openmct.router.off('change:params', this.changeSectionPage);
}, },
updated: function () { updated: function () {
this.$nextTick(() => { this.$nextTick(() => {
@ -284,14 +301,21 @@ export default {
this.sectionsChanged({ sections }); this.sectionsChanged({ sections });
this.resetSearch(); this.resetSearch();
}, },
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
this.selectSection(sectionId);
this.selectPage(pageId);
},
createNotebookStorageObject() { createNotebookStorageObject() {
const notebookMeta = { const notebookMeta = {
name: this.internalDomainObject.name, name: this.domainObject.name,
identifier: this.internalDomainObject.identifier, identifier: this.domainObject.identifier,
link: this.getLinktoNotebook() link: this.getLinktoNotebook()
}; };
const page = this.getSelectedPage(); const page = this.selectedPage;
const section = this.getSelectedSection(); const section = this.selectedSection;
return { return {
notebookMeta, notebookMeta,
@ -300,8 +324,7 @@ export default {
}; };
}, },
deleteEntry(entryId) { deleteEntry(entryId) {
const self = this; const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
if (entryPos === -1) { if (entryPos === -1) {
this.openmct.notifications.alert('Warning: unable to delete entry'); this.openmct.notifications.alert('Warning: unable to delete entry');
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`); console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
@ -317,9 +340,9 @@ export default {
label: "Ok", label: "Ok",
emphasis: true, emphasis: true,
callback: () => { callback: () => {
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage); const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.splice(entryPos, 1); entries.splice(entryPos, 1);
self.updateEntries(entries); this.updateEntries(entries);
dialog.dismiss(); dialog.dismiss();
} }
}, },
@ -395,6 +418,37 @@ export default {
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries; this.sidebarCoversEntries = sidebarCoversEntries;
}, },
getDefaultPageId() {
let defaultPageId;
if (this.isDefaultNotebook()) {
defaultPageId = getDefaultNotebook().page.id;
} else {
const firstSection = this.getSections()[0];
defaultPageId = firstSection && firstSection.pages[0].id;
}
return defaultPageId;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
return defaultNotebookIdentifier !== null
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
},
getDefaultSectionId() {
let defaultSectionId;
if (this.isDefaultNotebook()) {
defaultSectionId = getDefaultNotebook().section.id;
} else {
const firstSection = this.getSections()[0];
defaultSectionId = firstSection && firstSection.id;
}
return defaultSectionId;
},
getDefaultNotebookObject() { getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook(); const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) { if (!oldNotebookStorage) {
@ -423,14 +477,17 @@ export default {
getSection(id) { getSection(id) {
return this.sections.find(s => s.id === id); return this.sections.find(s => s.id === id);
}, },
getSections() {
return this.domainObject.configuration.sections || [];
},
getSearchResults() { getSearchResults() {
if (!this.search.length) { if (!this.search.length) {
return []; return [];
} }
const output = []; const output = [];
const sections = this.internalDomainObject.configuration.sections; const sections = this.domainObject.configuration.sections;
const entries = this.internalDomainObject.configuration.entries; const entries = this.domainObject.configuration.entries;
const searchTextLower = this.search.toLowerCase(); const searchTextLower = this.search.toLowerCase();
const originalSearchText = this.search; const originalSearchText = this.search;
let sectionTrackPageHit; let sectionTrackPageHit;
@ -509,77 +566,25 @@ export default {
this.searchResults = output; this.searchResults = output;
}, },
getPages() { getPages() {
const selectedSection = this.getSelectedSection(); const selectedSection = this.selectedSection;
if (!selectedSection || !selectedSection.pages.length) { if (!selectedSection || !selectedSection.pages.length) {
return []; return [];
} }
return selectedSection.pages; return selectedSection.pages;
}, },
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
let { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
const selectedSectionId = this.selectedSection && this.selectedSection.id;
const selectedPageId = this.selectedPage && this.selectedPage.id;
if (selectedPageId === pageId && selectedSectionId === sectionId) {
return;
}
this.sectionsChanged({ sections });
},
newEntry(embed = null) { newEntry(embed = null) {
this.resetSearch(); this.resetSearch();
const notebookStorage = this.createNotebookStorageObject(); const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage); this.updateDefaultNotebook(notebookStorage);
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed); const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
this.focusEntryId = id; this.focusEntryId = id;
}, },
orientationChange() { orientationChange() {
this.formatSidebar(); this.formatSidebar();
}, },
pagesChanged({ pages = [], id = null}) { pagesChanged({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection(); const selectedSection = this.selectedSection;
if (!selectedSection) { if (!selectedSection) {
return; return;
} }
@ -594,7 +599,6 @@ export default {
}); });
this.sectionsChanged({ sections }); this.sectionsChanged({ sections });
this.updateDefaultNotebookPage(pages, id);
}, },
removeDefaultClass(domainObject) { removeDefaultClass(domainObject) {
if (!domainObject) { if (!domainObject) {
@ -613,10 +617,10 @@ export default {
async updateDefaultNotebook(notebookStorage) { async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject(); const defaultNotebookObject = await this.getDefaultNotebookObject();
if (!defaultNotebookObject) { if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject); this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} }
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
@ -636,7 +640,7 @@ export default {
const notebookStorage = getDefaultNotebook(); const notebookStorage = getDefaultNotebook();
if (!notebookStorage if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return; return;
} }
@ -645,7 +649,7 @@ export default {
if (!page && defaultNotebookPage.id === id) { if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null; this.defaultSectionId = null;
this.defaultPageId = null; this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject); this.removeDefaultClass(this.domainObject);
clearDefaultNotebook(); clearDefaultNotebook();
return; return;
@ -664,7 +668,7 @@ export default {
const notebookStorage = getDefaultNotebook(); const notebookStorage = getDefaultNotebook();
if (!notebookStorage if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return; return;
} }
@ -673,7 +677,7 @@ export default {
if (!section && defaultNotebookSection.id === id) { if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null; this.defaultSectionId = null;
this.defaultPageId = null; this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject); this.removeDefaultClass(this.domainObject);
clearDefaultNotebook(); clearDefaultNotebook();
return; return;
@ -686,50 +690,46 @@ export default {
setDefaultNotebookSection(section); setDefaultNotebookSection(section);
}, },
updateEntry(entry) { updateEntry(entry) {
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage); const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage); const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos] = entry; entries[entryPos] = entry;
this.updateEntries(entries); this.updateEntries(entries);
}, },
updateEntries(entries) { updateEntries(entries) {
const configuration = this.internalDomainObject.configuration; const configuration = this.domainObject.configuration;
const notebookEntries = configuration.entries || {}; const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries; notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries); mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
}, },
updateInternalDomainObject(domainObject) { getPageIdFromUrl() {
this.internalDomainObject = domainObject; return this.openmct.router.getParams().pageId;
}, },
updateParams(sections) { getSectionIdFromUrl() {
const selectedSection = sections.find(s => s.isSelected); return this.openmct.router.getParams().sectionId;
if (!selectedSection) { },
return; syncUrlWithPageAndSection() {
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({ this.openmct.router.updateParams({
sectionId, pageId: this.selectedPageId,
pageId sectionId: this.selectedSectionId
}); });
}, },
sectionsChanged({ sections, id = null }) { sectionsChanged({ sections, id = null }) {
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections); mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id); this.updateDefaultNotebookSection(sections, id);
},
selectPage(pageId) {
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
},
selectSection(sectionId) {
this.selectedSectionId = sectionId;
const defaultPageId = this.selectedSection.pages[0].id;
this.selectPage(defaultPageId);
this.syncUrlWithPageAndSection();
} }
} }
}; };

View File

@ -4,7 +4,7 @@
class="c-ne__embed__snap-thumb" class="c-ne__embed__snap-thumb"
@click="openSnapshot()" @click="openSnapshot()"
> >
<img :src="embed.snapshot.src"> <img :src="thumbnailImage">
</div> </div>
<div class="c-ne__embed__info"> <div class="c-ne__embed__info">
<div class="c-ne__embed__name"> <div class="c-ne__embed__name">
@ -25,11 +25,15 @@
<script> <script>
import Moment from 'moment'; import Moment from 'moment';
import PopupMenu from './PopupMenu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction'; import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog'; import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance'; import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html'; import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import ImageExporter from '../../../exporters/ImageExporter';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue'; import Vue from 'vue';
export default { export default {
@ -59,11 +63,16 @@ export default {
computed: { computed: {
createdOn() { createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss'); return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
},
thumbnailImage() {
return this.embed.snapshot.thumbnailImage
? this.embed.snapshot.thumbnailImage.src
: this.embed.snapshot.src;
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems(); this.addPopupMenuItems();
this.exportImageService = this.openmct.$injector.get('exportImageService'); this.imageExporter = new ImageExporter(this.openmct);
}, },
methods: { methods: {
addPopupMenuItems() { addPopupMenuItems() {
@ -85,7 +94,7 @@ export default {
template: '<div id="snap-annotation"></div>' template: '<div id="snap-annotation"></div>'
}).$mount(); }).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot); const painterroInstance = new PainterroInstance(annotateVue.$el);
const annotateOverlay = this.openmct.overlays.overlay({ const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el, element: annotateVue.$el,
size: 'large', size: 'large',
@ -93,7 +102,6 @@ export default {
buttons: [ buttons: [
{ {
label: 'Cancel', label: 'Cancel',
emphasis: true,
callback: () => { callback: () => {
painterroInstance.dismiss(); painterroInstance.dismiss();
annotateOverlay.dismiss(); annotateOverlay.dismiss();
@ -101,11 +109,14 @@ export default {
}, },
{ {
label: 'Save', label: 'Save',
emphasis: true,
callback: () => { callback: () => {
painterroInstance.save(); painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss(); annotateOverlay.dismiss();
this.snapshotOverlay.dismiss(); this.snapshotOverlay.dismiss();
this.openSnapshot(); this.updateSnapshot(snapshotObject);
this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);
});
} }
} }
], ],
@ -115,7 +126,19 @@ export default {
}); });
painterroInstance.intialize(); painterroInstance.intialize();
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
painterroInstance.show(this.embed.snapshot.src); painterroInstance.show(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
}, },
changeLocation() { changeLocation() {
const hash = this.embed.historicLink; const hash = this.embed.historicLink;
@ -159,12 +182,29 @@ export default {
removeDialog.show(); removeDialog.show();
}, },
openSnapshot() { openSnapshot() {
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
this.openSnapshotOverlay(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
});
},
openSnapshotOverlay(src) {
const self = this; const self = this;
this.snapshot = new Vue({ this.snapshot = new Vue({
data: () => { data: () => {
return { return {
createdOn: this.createdOn, createdOn: this.createdOn,
embed: this.embed name: this.embed.name,
cssClass: this.embed.cssClass,
src
}; };
}, },
methods: { methods: {
@ -195,9 +235,9 @@ export default {
let element = this.snapshot.$refs['snapshot-image']; let element = this.snapshot.$refs['snapshot-image'];
if (type === 'png') { if (type === 'png') {
this.exportImageService.exportPNG(element, this.embed.name); this.imageExporter.exportPNG(element, this.embed.name);
} else { } else {
this.exportImageService.exportJPG(element, this.embed.name); this.imageExporter.exportJPG(element, this.embed.name);
} }
}, },
previewEmbed() { previewEmbed() {
@ -217,7 +257,9 @@ export default {
this.$emit('updateEmbed', embed); this.$emit('updateEmbed', embed);
}, },
updateSnapshot(snapshotObject) { updateSnapshot(snapshotObject) {
this.embed.snapshot = snapshotObject; this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.updateEmbed(this.embed); this.updateEmbed(this.embed);
} }
} }

View File

@ -62,7 +62,6 @@
<NotebookEmbed v-for="embed in entry.embeds" <NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id" :key="embed.id"
:embed="embed" :embed="embed"
:entry="entry"
@removeEmbed="removeEmbed" @removeEmbed="removeEmbed"
@updateEmbed="updateEmbed" @updateEmbed="updateEmbed"
/> />
@ -254,6 +253,7 @@ export default {
}, },
removeEmbed(id) { removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id); const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1); this.entry.embeds.splice(embedPosition, 1);
this.$emit('updateEntry', this.entry); this.$emit('updateEntry', this.entry);

View File

@ -80,7 +80,7 @@ export default {
notebookTypes.push({ notebookTypes.push({
cssClass: 'icon-notebook', cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`, name: `Save to Notebook ${defaultPath}`,
callBack: () => { onItemClicked: () => {
return this.snapshot(NOTEBOOK_DEFAULT); return this.snapshot(NOTEBOOK_DEFAULT);
} }
}); });
@ -89,7 +89,7 @@ export default {
notebookTypes.push({ notebookTypes.push({
cssClass: 'icon-camera', cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots', name: 'Save to Notebook Snapshots',
callBack: () => { onItemClicked: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT); return this.snapshot(NOTEBOOK_SNAPSHOT);
} }
}); });

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