Compare commits
	
		
			38 Commits
		
	
	
		
			mmgis-cust
			...
			plot-perfo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fd4dcc8513 | ||
|   | 9ebd18318b | ||
|   | 4a89b81f4f | ||
|   | 98e1abd7b1 | ||
|   | 56c25762ac | ||
|   | 5c8e726b87 | ||
|   | d80f4a1f7d | ||
|   | 3fe4c7a954 | ||
|   | 676ef60128 | ||
|   | 5a90d28450 | ||
|   | 2bb6822e6b | ||
|   | 383b4c0d8d | ||
|   | 404ab720ad | ||
|   | 259ab53060 | ||
|   | 1db7ac55b4 | ||
|   | 82b3383834 | ||
|   | ac240d524c | ||
|   | 1b034f6125 | ||
|   | b329ed6ed5 | ||
|   | 9b7a0d7e4c | ||
|   | 5c15e53abb | ||
|   | f58b3881f2 | ||
|   | 071a13b219 | ||
|   | ca66898e51 | ||
|   | 94c7b2343a | ||
|   | c397c336ab | ||
|   | eea23f2caf | ||
|   | 6665641c02 | ||
|   | c3ebf52dd2 | ||
|   | f8f2e7da9b | ||
|   | 240f58b2d0 | ||
|   | 7d3baee7b5 | ||
|   | 1f5cb7ca42 | ||
|   | 4a7ebe326c | ||
|   | 10da314a4a | ||
|   | b3ceccd7fb | ||
|   | 1bde4c9a0c | ||
|   | 4b85360446 | 
| @@ -1,36 +1,69 @@ | ||||
| version: 2 | ||||
| jobs: | ||||
|   build: | ||||
| version: 2.1 | ||||
| executors: | ||||
|   linux: | ||||
|     docker: | ||||
|         - image: circleci/node:13-browsers | ||||
|           environment: | ||||
|             CHROME_BIN: "/usr/bin/google-chrome" | ||||
|     steps: | ||||
|         - checkout | ||||
|         - run: | ||||
|             name: Update npm | ||||
|             command: 'sudo npm install -g npm@latest' | ||||
|         - restore_cache: | ||||
|             key: dependency-cache-{{ checksum "package.json" }} | ||||
|         - run: | ||||
|             name: Installing dependencies (npm install) | ||||
|             command: npm install | ||||
|         - save_cache: | ||||
|             key: dependency-cache-{{ checksum "package.json" }} | ||||
|             paths: | ||||
|               - node_modules | ||||
|         - run: | ||||
|             name: npm run test:coverage | ||||
|             command: npm run test:coverage | ||||
|         - run: | ||||
|             name: npm run lint | ||||
|             command: npm run lint | ||||
|         - store_artifacts: | ||||
|             path: dist | ||||
|             prefix: dist | ||||
|  | ||||
| workflows: | ||||
|   version: 2 | ||||
|       - image: cimg/base:stable | ||||
| orbs: | ||||
|   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: | ||||
|       - checkout | ||||
|       - restore_cache: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }} | ||||
|       - node/install: | ||||
|           node-version: << parameters.node-version >> | ||||
|       - 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: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }} | ||||
|           paths: | ||||
|             - ~/.npm | ||||
|             - ~/.cache | ||||
|             - node_modules | ||||
|       - run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|       - store_artifacts: | ||||
|           path: dist/reports/ | ||||
| workflows: | ||||
|   matrix-tests: | ||||
|     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 | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -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 ! | ||||
| title: '' | ||||
| labels: type:bug | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| <!--- Focus on user impact in the title. Use the Summary Field to --> | ||||
| <!--- describe the problem technically. --> | ||||
|  | ||||
| @@ -35,7 +36,7 @@ about: File a Bug ! | ||||
|  | ||||
| #### Environment | ||||
| * 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: | ||||
| * Browser: | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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? | ||||
|   | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/enhancement-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/enhancement-request.md
									
									
									
									
										vendored
									
									
										Normal 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. | ||||
							
								
								
									
										4
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,8 @@ jobs: | ||||
|       - uses: actions/checkout@v2 | ||||
|         with: | ||||
|           ref: ${{ github.event.inputs.version }} | ||||
|       - uses: actions/setup-node@v1 | ||||
|       - 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 | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Vue from 'Vue'; | ||||
| import Vue from 'vue'; | ||||
| import HelloWorld from './HelloWorld.vue'; | ||||
|  | ||||
| function SimpleVuePlugin() { | ||||
|   | ||||
| @@ -88,6 +88,7 @@ | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.PlanLayout()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.Hyperlink()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
| @@ -194,6 +195,7 @@ | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], | ||||
|             {indicator: true} | ||||
|         )); | ||||
|         openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); | ||||
|         openmct.start(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
| @@ -23,9 +23,9 @@ | ||||
| /*global module,process*/ | ||||
|  | ||||
| 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 reporters = ['progress', 'html']; | ||||
| const reporters = ['progress', 'html', 'junit']; | ||||
|  | ||||
| if (coverageEnabled) { | ||||
|     reporters.push('coverage-istanbul'); | ||||
| @@ -59,7 +59,8 @@ module.exports = (config) => { | ||||
|         browsers: browsers, | ||||
|         client: { | ||||
|             jasmine: { | ||||
|                 random: false | ||||
|                 random: false, | ||||
|                 timeoutInterval: 30000 | ||||
|             } | ||||
|         }, | ||||
|         customLaunchers: { | ||||
| @@ -67,6 +68,10 @@ module.exports = (config) => { | ||||
|                 base: 'Chrome', | ||||
|                 flags: ['--remote-debugging-port=9222'], | ||||
|                 debug: true | ||||
|             }, | ||||
|             FirefoxESR: { | ||||
|                 base: 'FirefoxHeadless', | ||||
|                 name: 'FirefoxESR' | ||||
|             } | ||||
|         }, | ||||
|         colors: true, | ||||
| @@ -78,12 +83,21 @@ module.exports = (config) => { | ||||
|             preserveDescribeNesting: true, | ||||
|             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: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: process.env.CIRCLE_ARTIFACTS ? | ||||
|                 process.env.CIRCLE_ARTIFACTS + '/coverage' : | ||||
|                 "dist/reports/coverage", | ||||
|             dir: process.env.CIRCLE_ARTIFACTS | ||||
|                 ? process.env.CIRCLE_ARTIFACTS + '/coverage' | ||||
|                 : "dist/reports/coverage", | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|   | ||||
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,10 +1,8 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.4", | ||||
|   "version": "1.7.6-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": { | ||||
|     "openmct-mmgis": "git+https://trunk.arc.nasa.gov/bitbucket/scm/vipergds/openmct-mmgis.git#api-mmgis-inspector" | ||||
|   }, | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
| @@ -39,17 +37,18 @@ | ||||
|     "html2canvas": "^1.0.0-rc.7", | ||||
|     "imports-loader": "^0.8.0", | ||||
|     "istanbul-instrumenter-loader": "^3.0.1", | ||||
|     "jasmine-core": "^3.1.0", | ||||
|     "jasmine-core": "^3.7.1", | ||||
|     "jsdoc": "^3.3.2", | ||||
|     "karma": "5.1.1", | ||||
|     "karma": "6.3.4", | ||||
|     "karma-chrome-launcher": "3.1.0", | ||||
|     "karma-firefox-launcher": "2.1.1", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.0.3", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "1.3.0", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-html-reporter": "0.2.7", | ||||
|     "karma-jasmine": "3.3.1", | ||||
|     "karma-sourcemap-loader": "0.3.7", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-webpack": "4.0.2", | ||||
|     "location-bar": "^3.0.1", | ||||
|     "lodash": "^4.17.12", | ||||
| @@ -91,6 +90,7 @@ | ||||
|     "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: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", | ||||
|     "verify": "concurrently 'npm:test' 'npm:lint'", | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", | ||||
| @@ -102,6 +102,9 @@ | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/nasa/openmct.git" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=10.10.2 <16.0.0" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "Apache-2.0", | ||||
|   "private": true | ||||
|   | ||||
| @@ -21,32 +21,24 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "moment-timezone", | ||||
|     "./src/indicators/ClockIndicator", | ||||
|     "./src/services/TickerService", | ||||
|     "./src/services/TimerService", | ||||
|     "./src/controllers/ClockController", | ||||
|     "./src/controllers/TimerController", | ||||
|     "./src/controllers/RefreshingController", | ||||
|     "./src/actions/StartTimerAction", | ||||
|     "./src/actions/RestartTimerAction", | ||||
|     "./src/actions/StopTimerAction", | ||||
|     "./src/actions/PauseTimerAction", | ||||
|     "./res/templates/clock.html", | ||||
|     "./res/templates/timer.html" | ||||
| ], function ( | ||||
|     MomentTimezone, | ||||
|     ClockIndicator, | ||||
|     TickerService, | ||||
|     TimerService, | ||||
|     ClockController, | ||||
|     TimerController, | ||||
|     RefreshingController, | ||||
|     StartTimerAction, | ||||
|     RestartTimerAction, | ||||
|     StopTimerAction, | ||||
|     PauseTimerAction, | ||||
|     clockTemplate, | ||||
|     timerTemplate | ||||
| ) { | ||||
|     return { | ||||
| @@ -73,16 +65,6 @@ define([ | ||||
|                         "value": "YYYY/MM/DD HH:mm:ss" | ||||
|                     } | ||||
|                 ], | ||||
|                 "indicators": [ | ||||
|                     { | ||||
|                         "implementation": ClockIndicator, | ||||
|                         "depends": [ | ||||
|                             "tickerService", | ||||
|                             "CLOCK_INDICATOR_FORMAT" | ||||
|                         ], | ||||
|                         "priority": "preferred" | ||||
|                     } | ||||
|                 ], | ||||
|                 "services": [ | ||||
|                     { | ||||
|                         "key": "tickerService", | ||||
| @@ -99,14 +81,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "controllers": [ | ||||
|                     { | ||||
|                         "key": "ClockController", | ||||
|                         "implementation": ClockController, | ||||
|                         "depends": [ | ||||
|                             "$scope", | ||||
|                             "tickerService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "TimerController", | ||||
|                         "implementation": TimerController, | ||||
| @@ -126,12 +100,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "views": [ | ||||
|                     { | ||||
|                         "key": "clock", | ||||
|                         "type": "clock", | ||||
|                         "editable": false, | ||||
|                         "template": clockTemplate | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "timer", | ||||
|                         "type": "timer", | ||||
| @@ -181,75 +149,11 @@ define([ | ||||
|                         ], | ||||
|                         "category": "contextual", | ||||
|                         "name": "Stop", | ||||
|                         "cssClass": "icon-box", | ||||
|                         "cssClass": "icon-box-round-corners", | ||||
|                         "priority": "preferred" | ||||
|                     } | ||||
|                 ], | ||||
|                 "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", | ||||
|                         "name": "Timer", | ||||
|   | ||||
| @@ -1,32 +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. | ||||
| --> | ||||
| <div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock"> | ||||
| 	<div class="c-clock__timezone"> | ||||
| 		{{clock.zone()}} | ||||
| 	</div> | ||||
| 	<div class="c-clock__value"> | ||||
| 		{{clock.text()}} | ||||
| 	</div> | ||||
| 	<div class="c-clock__ampm"> | ||||
| 		{{clock.ampm()}} | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -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; | ||||
| } | ||||
| ); | ||||
| @@ -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; | ||||
|     } | ||||
| ); | ||||
| @@ -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(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -101,7 +101,7 @@ define( | ||||
|                     name: "Pause" | ||||
|                 }); | ||||
|                 mockStop.getMetadata.and.returnValue({ | ||||
|                     cssClass: "icon-box", | ||||
|                     cssClass: "icon-box-round-corners", | ||||
|                     name: "Stop" | ||||
|                 }); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|   | ||||
| @@ -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)); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -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"] | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| }); | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| ); | ||||
| @@ -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); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| @@ -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> | ||||
| @@ -122,6 +122,7 @@ define([ | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|         /** | ||||
|          * Tracks current selection state of the application. | ||||
|          * @private | ||||
| @@ -262,7 +263,7 @@ define([ | ||||
|         // Plugins that are installed by default | ||||
|  | ||||
|         this.install(this.plugins.Plot()); | ||||
|         this.install(this.plugins.TelemetryTable()); | ||||
|         this.install(this.plugins.TelemetryTable.default()); | ||||
|         this.install(PreviewPlugin.default()); | ||||
|         this.install(LegacyIndicatorsPlugin()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
| @@ -283,6 +284,7 @@ define([ | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|         this.install(this.plugins.ViewLargeAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.NonEditableFolder()); | ||||
|     } | ||||
| @@ -434,6 +436,8 @@ define([ | ||||
|                     Browse(this); | ||||
|                 } | ||||
|  | ||||
|                 window.addEventListener('beforeunload', this.destroy); | ||||
|  | ||||
|                 this.router.start(); | ||||
|                 this.emit('start'); | ||||
|             }.bind(this)); | ||||
| @@ -457,6 +461,7 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     MCT.prototype.destroy = function () { | ||||
|         window.removeEventListener('beforeunload', this.destroy); | ||||
|         this.emit('destroy'); | ||||
|         this.router.destroy(); | ||||
|     }; | ||||
|   | ||||
| @@ -36,8 +36,7 @@ define([ | ||||
|     './views/installLegacyViews', | ||||
|     './policies/LegacyCompositionPolicyAdapter', | ||||
|     './actions/LegacyActionAdapter', | ||||
|     './services/LegacyPersistenceAdapter', | ||||
|     './services/ExportImageService' | ||||
|     './services/LegacyPersistenceAdapter' | ||||
| ], function ( | ||||
|     ActionDialogDecorator, | ||||
|     AdapterCapability, | ||||
| @@ -54,8 +53,7 @@ define([ | ||||
|     installLegacyViews, | ||||
|     legacyCompositionPolicyAdapter, | ||||
|     LegacyActionAdapter, | ||||
|     LegacyPersistenceAdapter, | ||||
|     ExportImageService | ||||
|     LegacyPersistenceAdapter | ||||
| ) { | ||||
|     return { | ||||
|         name: 'src/adapter', | ||||
| @@ -84,13 +82,6 @@ define([ | ||||
|                             "identifierService", | ||||
|                             "cacheService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "exportImageService", | ||||
|                         "implementation": ExportImageService, | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 components: [ | ||||
|   | ||||
| @@ -173,10 +173,11 @@ define([ | ||||
|         const limitEvaluator = oldObject.getCapability("limit"); | ||||
|  | ||||
|         return { | ||||
|             limits: function () { | ||||
|                 return limitEvaluator.limits(); | ||||
|             limits: () => { | ||||
|                 return limitEvaluator.limits.then !== undefined | ||||
|                     ? limitEvaluator.limits() | ||||
|                     : Promise.resolve(limitEvaluator.limits()); | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -1,218 +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 {object} options Image options. | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) { | ||||
|             const self = this; | ||||
|             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) { | ||||
|                     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 = dialogService.showBlockingMessage({ | ||||
|                     title: "Error capturing image", | ||||
|                     severity: "error", | ||||
|                     hint: "Image was not captured successfully!", | ||||
|                     options: [{ | ||||
|                         label: "OK", | ||||
|                         callback: function () { | ||||
|                             errorDialog.dismiss(); | ||||
|                         } | ||||
|                     }] | ||||
|                 }); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         ExportImageService.prototype.getThumbnail = function (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} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportJPG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'jpg', | ||||
|                 className | ||||
|             }) | ||||
|                 .then(function (img) { | ||||
|                     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} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportPNG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'png', | ||||
|                 className | ||||
|             }) | ||||
|                 .then(function (img) { | ||||
|                     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} | ||||
|          */ | ||||
|  | ||||
|         ExportImageService.prototype.exportPNGtoSRC = function (element, options) { | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'png', | ||||
|                 ...options | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         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; | ||||
|     } | ||||
| ); | ||||
| @@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter { | ||||
|             this._observeObjectPath(); | ||||
|             this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this._initializeActions(); | ||||
|     } | ||||
|  | ||||
|     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() { | ||||
|         let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); | ||||
|  | ||||
|         this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); | ||||
|         this._initializeActions(); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter { | ||||
|         this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; | ||||
|  | ||||
|         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._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); | ||||
|     } | ||||
| @@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter { | ||||
|         this._allActions[actionDefinition.key] = actionDefinition; | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         if (view) { | ||||
|     getAction(key) { | ||||
|         return this._allActions[key]; | ||||
|     } | ||||
|  | ||||
|     getActionsCollection(objectPath, view) { | ||||
|         if (view) { | ||||
|             return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); | ||||
|         } else { | ||||
|  | ||||
|             return this._newActionCollection(objectPath, view, true); | ||||
|         } | ||||
|     } | ||||
| @@ -57,15 +59,6 @@ class ActionsAPI extends EventEmitter { | ||||
|         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) { | ||||
|         let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
| @@ -75,7 +68,17 @@ class ActionsAPI extends EventEmitter { | ||||
|     _newActionCollection(objectPath, view, skipEnvironmentObservers) { | ||||
|         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) { | ||||
|   | ||||
| @@ -106,7 +106,7 @@ describe('The Actions API', () => { | ||||
|         it("adds action to ActionsAPI", () => { | ||||
|             actionsAPI.register(mockAction); | ||||
|  | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[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", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         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; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         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]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockObjectPathAction.key); | ||||
| @@ -143,7 +143,7 @@ describe('The Actions API', () => { | ||||
|         }); | ||||
|  | ||||
|         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]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js'; | ||||
|  * @property {Boolean} isDisabled adds disable class if true | ||||
|  * @property {String} name Menu item text | ||||
|  * @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 {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu | ||||
|      */ | ||||
|     showMenu(x, y, actions, menuOptions) { | ||||
|         this._createMenuComponent(x, y, actions, menuOptions); | ||||
|     showMenu(x, y, items, menuOptions) { | ||||
|         this._createMenuComponent(x, y, items, menuOptions); | ||||
|  | ||||
|         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 | ||||
|      * @param {number} x x-coordinates for popup | ||||
|   | ||||
| @@ -57,7 +57,7 @@ describe ('The Menu API', () => { | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
|             }, | ||||
| @@ -66,7 +66,7 @@ describe ('The Menu API', () => { | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|                 @click="action.onItemClicked" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
|             </li> | ||||
| @@ -36,7 +36,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|             @click="action.onItemClicked" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|                 @click="action.onItemClicked" | ||||
|                 @mouseover="toggleItemDescription(action)" | ||||
|                 @mouseleave="toggleItemDescription()" | ||||
|             > | ||||
| @@ -42,7 +42,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|             @click="action.onItemClicked" | ||||
|             @mouseover="toggleItemDescription(action)" | ||||
|             @mouseleave="toggleItemDescription()" | ||||
|         > | ||||
|   | ||||
| @@ -71,12 +71,12 @@ class Menu extends EventEmitter { | ||||
|  | ||||
|     showMenu() { | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
| @@ -85,12 +85,12 @@ class Menu extends EventEmitter { | ||||
|  | ||||
|     showSuperMenu() { | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             components: { | ||||
|                 SuperMenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             template: '<super-menu-component />' | ||||
|         }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										185
									
								
								src/exporters/ImageExporter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/exporters/ImageExporter.js
									
									
									
									
									
										Normal 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; | ||||
|  | ||||
							
								
								
									
										58
									
								
								src/exporters/ImageExporterSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/exporters/ImageExporterSpec.js
									
									
									
									
									
										Normal 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); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -38,8 +38,6 @@ const DEFAULTS = [ | ||||
|     'platform/exporters', | ||||
|     'platform/telemetry', | ||||
|     'platform/features/clock', | ||||
|     'platform/features/hyperlink', | ||||
|     'platform/features/timeline', | ||||
|     'platform/forms', | ||||
|     'platform/identity', | ||||
|     'platform/persistence/aggregator', | ||||
| @@ -82,9 +80,7 @@ define([ | ||||
|     '../platform/exporters/bundle', | ||||
|     '../platform/features/clock/bundle', | ||||
|     '../platform/features/my-items/bundle', | ||||
|     '../platform/features/hyperlink/bundle', | ||||
|     '../platform/features/static-markup/bundle', | ||||
|     '../platform/features/timeline/bundle', | ||||
|     '../platform/forms/bundle', | ||||
|     '../platform/framework/bundle', | ||||
|     '../platform/framework/src/load/Bundle', | ||||
|   | ||||
| @@ -19,8 +19,8 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * 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) { | ||||
|     return { | ||||
| @@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) { | ||||
|             return domainObject.type === 'LadTableSet'; | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             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; | ||||
|                 } | ||||
|             }; | ||||
|             return new LadTableSetView(openmct, domainObject, objectPath); | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/plugins/LADTable/LADTableView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/plugins/LADTable/LADTableView.js
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
| @@ -19,50 +19,30 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import LadTable from './components/LADTable.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function LADTableViewProvider(openmct) { | ||||
|     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; | ||||
| import LADTableView from './LADTableView'; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             LadTableComponent: LadTable | ||||
|                         }, | ||||
|                         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; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| export default class LADTableViewProvider { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.name = 'LAD Table'; | ||||
|         this.key = 'LadTable'; | ||||
|         this.cssClass = 'icon-tabular-lad'; | ||||
|     } | ||||
|  | ||||
|     canView(domainObject) { | ||||
|         return domainObject.type === 'LadTable'; | ||||
|     } | ||||
|  | ||||
|     canEdit(domainObject) { | ||||
|         return domainObject.type === 'LadTable'; | ||||
|     } | ||||
|  | ||||
|     view(domainObject, objectPath) { | ||||
|         return new LADTableView(this.openmct, domainObject, objectPath); | ||||
|     } | ||||
|  | ||||
|     priority(domainObject) { | ||||
|         return 1; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/plugins/LADTable/LadTableSetView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/plugins/LADTable/LadTableSetView.js
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
| @@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [ | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -167,25 +167,23 @@ export default { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: () => { | ||||
|                             return this.datum; | ||||
|                         } | ||||
|                     }; | ||||
|         updateViewContext() { | ||||
|             this.$emit('rowContextClick', { | ||||
|                 viewHistoricalData: true, | ||||
|                 viewDatumAction: true, | ||||
|                 getDatum: () => { | ||||
|                     return this.datum; | ||||
|                 } | ||||
|             }; | ||||
|             }); | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             let actionCollection = this.openmct.actions.get(this.objectPath, this.getView()); | ||||
|             let allActions = actionCollection.getActionsObject(); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|             this.updateViewContext(); | ||||
|  | ||||
|             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() { | ||||
|             this.value = '---'; | ||||
|   | ||||
| @@ -38,6 +38,7 @@ | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|                 @rowContextClick="updateViewContext" | ||||
|             /> | ||||
|         </tbody> | ||||
|     </table> | ||||
| @@ -51,7 +52,7 @@ export default { | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -64,7 +65,8 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             items: [] | ||||
|             items: [], | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -114,6 +116,12 @@ export default { | ||||
|             let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit); | ||||
|  | ||||
|             return metadataWithUnits.length > 0; | ||||
|         }, | ||||
|         updateViewContext(rowContext) { | ||||
|             this.viewContext.row = rowContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -48,6 +48,7 @@ | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="ladTable.objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|                 @rowContextClick="updateViewContext" | ||||
|             /> | ||||
|         </template> | ||||
|     </tbody> | ||||
| @@ -61,7 +62,7 @@ export default { | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -72,7 +73,8 @@ export default { | ||||
|         return { | ||||
|             ladTableObjects: [], | ||||
|             ladTelemetryObjects: {}, | ||||
|             compositions: [] | ||||
|             compositions: [], | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -166,6 +168,12 @@ export default { | ||||
|  | ||||
|                 this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects); | ||||
|             }; | ||||
|         }, | ||||
|         updateViewContext(rowContext) { | ||||
|             this.viewContext.row = rowContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
|         this.updateTimeSettings(); | ||||
|         this.openmct.router.on('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         TIME_EVENTS.forEach(event => { | ||||
|   | ||||
| @@ -20,33 +20,40 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| define([ | ||||
|     "./res/templates/deprecated-timeline-message.html" | ||||
| ], function ( | ||||
|     deprecatedTimelineMessage | ||||
| ) { | ||||
| import Clock from './components/Clock.vue'; | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default function ClockViewProvider(openmct) { | ||||
|     return { | ||||
|         name: 'platform/features/timeline', | ||||
|         definition: { | ||||
|             extensions: { | ||||
|                 types: [ | ||||
|                     { | ||||
|                         key: "timeline", | ||||
|                         name: "Timeline", | ||||
|                         description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)", | ||||
|                         priority: 502 | ||||
|                     } | ||||
|                 ], | ||||
|                 views: [ | ||||
|                     { | ||||
|                         key: "timeline", | ||||
|                         name: "Timeline", | ||||
|                         type: "timeline", | ||||
|                         description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)", | ||||
|                         template: deprecatedTimelineMessage | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         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; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| }); | ||||
| } | ||||
							
								
								
									
										99
									
								
								src/plugins/clock/components/Clock.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/plugins/clock/components/Clock.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -19,10 +19,46 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  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}}" | ||||
|    ng-class="{ | ||||
|    'c-hyperlink--button u-fills-container' : hyperlink.isButton(), | ||||
|    'c-hyperlink--link' : !hyperlink.isButton() }"> | ||||
|     <span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span> | ||||
| </a> | ||||
| 
 | ||||
| <template> | ||||
| <div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"> | ||||
|     <span class="label c-indicator__label"> | ||||
|         {{ timeTextValue }} | ||||
|     </span> | ||||
| </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
									
								
							
							
						
						
									
										154
									
								
								src/plugins/clock/plugin.js
									
									
									
									
									
										Normal 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; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     }; | ||||
| } | ||||
| @@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter { | ||||
|         this.subscriptions = {}; | ||||
|         this.telemetryObjects = {}; | ||||
|         this.testData = { | ||||
|             conditionTestData: [], | ||||
|             conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, | ||||
|             applied: false | ||||
|         }; | ||||
|         this.initialize(); | ||||
| @@ -154,8 +154,10 @@ export default class ConditionManager extends EventEmitter { | ||||
|  | ||||
|     updateConditionDescription(condition) { | ||||
|         const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id)); | ||||
|         found.summary = condition.description; | ||||
|         this.persistConditions(); | ||||
|         if (found.summary !== condition.description) { | ||||
|             found.summary = condition.description; | ||||
|             this.persistConditions(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     initCondition(conditionConfiguration, index) { | ||||
| @@ -414,8 +416,10 @@ export default class ConditionManager extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     updateTestData(testData) { | ||||
|         this.testData = testData; | ||||
|         this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); | ||||
|         if (!_.isEqual(testData, this.testData)) { | ||||
|             this.testData = testData; | ||||
|             this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     persistConditions() { | ||||
|   | ||||
| @@ -215,7 +215,8 @@ export default { | ||||
|         }, | ||||
|         isEditing: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|             required: true, | ||||
|             default: false | ||||
|         }, | ||||
|         telemetry: { | ||||
|             type: Array, | ||||
|   | ||||
| @@ -20,71 +20,78 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './components/AlphanumericFormatView.vue', | ||||
|     'vue' | ||||
| ], function (AlphanumericFormatView, Vue) { | ||||
| import AlphanumericFormat from './components/AlphanumericFormat.vue'; | ||||
|  | ||||
|     function AlphanumericFormatViewProvider(openmct, options) { | ||||
|         function isTelemetryObject(selectionPath) { | ||||
|             let selectedObject = selectionPath[0].context.item; | ||||
|             let parentObject = selectionPath[1].context.item; | ||||
|             let selectedLayoutItem = selectionPath[0].context.layoutItem; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
|             return parentObject | ||||
|                 && parentObject.type === 'layout' | ||||
|                 && selectedObject | ||||
|                 && selectedLayoutItem | ||||
|                 && selectedLayoutItem.type === 'telemetry-view' | ||||
|                 && openmct.telemetry.isTelemetryObject(selectedObject) | ||||
|                 && !options.showAsView.includes(selectedObject.type); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             key: 'alphanumeric-format', | ||||
|             name: 'Alphanumeric Format', | ||||
|             canView: function (selection) { | ||||
|                 if (selection.length === 0 || selection[0].length === 1) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 return selection.every(isTelemetryObject); | ||||
|             }, | ||||
|             view: function (domainObject, objectPath) { | ||||
|                 let component; | ||||
|  | ||||
|                 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 () { | ||||
|                 return 1; | ||||
|             } | ||||
|         }; | ||||
| class AlphanumericFormatView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     return AlphanumericFormatViewProvider; | ||||
| }); | ||||
|     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) { | ||||
|         let selectedObject = selectionPath[0].context.item; | ||||
|         let parentObject = selectionPath[1].context.item; | ||||
|         let selectedLayoutItem = selectionPath[0].context.layoutItem; | ||||
|  | ||||
|         return parentObject | ||||
|             && parentObject.type === 'layout' | ||||
|             && selectedObject | ||||
|             && selectedLayoutItem | ||||
|             && selectedLayoutItem.type === 'telemetry-view' | ||||
|             && openmct.telemetry.isTelemetryObject(selectedObject) | ||||
|             && !options.showAsView.includes(selectedObject.type); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: 'alphanumeric-format', | ||||
|         name: 'Alphanumeric Format', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 1) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return selection.every(isTelemetryObject); | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             return new AlphanumericFormatView(openmct, domainObject, objectPath); | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ export default class CopyToClipboardAction { | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|         const formattedValue = viewContext.row.formattedValueForCopy(); | ||||
|  | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
| @@ -26,9 +26,13 @@ export default class CopyToClipboardAction { | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|         return row.formattedValueForCopy | ||||
|             && typeof row.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,8 @@ | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     name: 'AlphanumericFormat', | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing(), | ||||
| @@ -56,6 +56,7 @@ | ||||
|         :index="index" | ||||
|         :multi-select="selectedLayoutItems.length > 1" | ||||
|         :is-editing="isEditing" | ||||
|         @contextClick="updateViewContext" | ||||
|         @move="move" | ||||
|         @endMove="endMove" | ||||
|         @endLineResize="endLineResize" | ||||
| @@ -140,7 +141,7 @@ function getItemDefinition(itemType, ...options) { | ||||
|  | ||||
| export default { | ||||
|     components: components, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -155,7 +156,8 @@ export default { | ||||
|         return { | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
|             showGrid: true, | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -819,6 +821,12 @@ export default { | ||||
|         }, | ||||
|         toggleGrid() { | ||||
|             this.showGrid = !this.showGrid; | ||||
|         }, | ||||
|         updateViewContext(viewContext) { | ||||
|             this.viewContext.row = viewContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -102,7 +102,7 @@ export default { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'currentView'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -294,16 +294,6 @@ export default { | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         formattedValueForCopy: this.formattedValueForCopy | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.mutablePromise = undefined; | ||||
| @@ -338,30 +328,38 @@ export default { | ||||
|  | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         updateViewContext() { | ||||
|             this.$emit('contextClick', { | ||||
|                 viewHistoricalData: true, | ||||
|                 formattedValueForCopy: this.formattedValueForCopy | ||||
|             }); | ||||
|         }, | ||||
|         async getContextMenuActions() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             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) { | ||||
|                 const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                 copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`; | ||||
|             } else { | ||||
|                 actionsObject.copyToNotebook = undefined; | ||||
|                 delete actionsObject.copyToNotebook; | ||||
|                 defaultNotebookName = `Copy to Notebook ${defaultPath}`; | ||||
|             } | ||||
|  | ||||
|             return CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return actionsObject[actionKey]; | ||||
|             }).filter(action => action !== undefined); | ||||
|             return CONTEXT_MENU_ACTIONS | ||||
|                 .map(actionKey => { | ||||
|                     const action = this.openmct.actions.getAction(actionKey); | ||||
|                     if (action.key === 'copyToNotebook') { | ||||
|                         action.name = defaultNotebookName; | ||||
|                     } | ||||
|  | ||||
|                     return action; | ||||
|                 }) | ||||
|                 .filter(action => action.name !== undefined); | ||||
|         }, | ||||
|         async showContextMenu(event) { | ||||
|             this.updateViewContext(); | ||||
|             const contextMenuActions = await this.getContextMenuActions(); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); | ||||
|             const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|   | ||||
| @@ -20,13 +20,81 @@ | ||||
|  * 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 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) { | ||||
|     return function (openmct) { | ||||
| @@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) { | ||||
|                 return domainObject.type === 'layout'; | ||||
|             }, | ||||
|             view: function (domainObject, objectPath) { | ||||
|                 let component; | ||||
|  | ||||
|                 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(); | ||||
|                     } | ||||
|                 }; | ||||
|                 return new DisplayLayoutView(openmct, domainObject, objectPath, options); | ||||
|             }, | ||||
|             priority() { | ||||
|                 return 100; | ||||
|   | ||||
							
								
								
									
										51
									
								
								src/plugins/hyperlink/HyperlinkLayout.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/plugins/hyperlink/HyperlinkLayout.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										59
									
								
								src/plugins/hyperlink/HyperlinkProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/plugins/hyperlink/HyperlinkProvider.js
									
									
									
									
									
										Normal 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 HyperlinkLayout from './HyperlinkLayout.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function HyperlinkProvider(openmct) { | ||||
|  | ||||
|     return { | ||||
|         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; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										89
									
								
								src/plugins/hyperlink/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/plugins/hyperlink/plugin.js
									
									
									
									
									
										Normal 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)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										130
									
								
								src/plugins/hyperlink/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/plugins/hyperlink/pluginSpec.js
									
									
									
									
									
										Normal 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'); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										37
									
								
								src/plugins/imagery/ImageryView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/plugins/imagery/ImageryView.js
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
| @@ -19,9 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ImageryViewLayout from './components/ImageryViewLayout.vue'; | ||||
| import Vue from 'vue'; | ||||
| import ImageryView from './ImageryView'; | ||||
|  | ||||
| export default function ImageryViewProvider(openmct) { | ||||
|     const type = 'example.imagery'; | ||||
| @@ -42,31 +40,8 @@ export default function ImageryViewProvider(openmct) { | ||||
|         canView: function (domainObject) { | ||||
|             return hasImageTelemetry(domainObject); | ||||
|         }, | ||||
|         view: function (domainObject) { | ||||
|             let component; | ||||
|  | ||||
|             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; | ||||
|                 }, | ||||
|                 _getInstance: function () { | ||||
|                     return component; | ||||
|                 } | ||||
|             }; | ||||
|         view: function (domainObject, objectPath) { | ||||
|             return new ImageryView(openmct, domainObject, objectPath); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,7 @@ | ||||
|         <div ref="imageBG" | ||||
|              class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|              @click="expand" | ||||
|         > | ||||
|             <div class="image-wrapper" | ||||
|                  :style="{ | ||||
| @@ -170,8 +171,9 @@ | ||||
| <script> | ||||
| import _ from 'lodash'; | ||||
| import moment from 'moment'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
|  | ||||
| import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| @@ -195,7 +197,7 @@ export default { | ||||
|     components: { | ||||
|         Compass | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|  | ||||
| @@ -468,6 +470,16 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     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() { | ||||
|             this.relatedTelemetry = new RelatedTelemetry( | ||||
|                 this.openmct, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| .c-imagery { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex: 1 1 auto; | ||||
|     height: 100%; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     &:focus { | ||||
|   | ||||
| @@ -280,7 +280,7 @@ describe("The Imagery View Layout", () => { | ||||
|             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; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|             Vue.nextTick(() => { | ||||
| @@ -317,7 +317,7 @@ describe("The Imagery View Layout", () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via arrow keys", (done) => { | ||||
|         xit("should navigate via arrow keys", (done) => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
|   | ||||
| @@ -25,16 +25,20 @@ export default class CopyToNotebookAction { | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|     invoke(objectPath, view) { | ||||
|         const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy; | ||||
|  | ||||
|         this.copyToNotebook(viewContext.formattedValueForCopy()); | ||||
|         this.copyToNotebook(formattedValueForCopy()); | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|         return row.formattedValueForCopy | ||||
|             && typeof row.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import PainterroInstance from '../utils/painterroInstance'; | ||||
| 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'; | ||||
| @@ -71,7 +72,7 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.addPopupMenuItems(); | ||||
|         this.exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|     }, | ||||
|     methods: { | ||||
|         addPopupMenuItems() { | ||||
| @@ -101,7 +102,6 @@ export default { | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Cancel', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             painterroInstance.dismiss(); | ||||
|                             annotateOverlay.dismiss(); | ||||
| @@ -109,6 +109,7 @@ export default { | ||||
|                     }, | ||||
|                     { | ||||
|                         label: 'Save', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             painterroInstance.save((snapshotObject) => { | ||||
|                                 annotateOverlay.dismiss(); | ||||
| @@ -234,9 +235,9 @@ export default { | ||||
|             let element = this.snapshot.$refs['snapshot-image']; | ||||
|  | ||||
|             if (type === 'png') { | ||||
|                 this.exportImageService.exportPNG(element, this.embed.name); | ||||
|                 this.imageExporter.exportPNG(element, this.embed.name); | ||||
|             } else { | ||||
|                 this.exportImageService.exportJPG(element, this.embed.name); | ||||
|                 this.imageExporter.exportJPG(element, this.embed.name); | ||||
|             } | ||||
|         }, | ||||
|         previewEmbed() { | ||||
|   | ||||
| @@ -80,7 +80,7 @@ export default { | ||||
|                 notebookTypes.push({ | ||||
|                     cssClass: 'icon-notebook', | ||||
|                     name: `Save to Notebook ${defaultPath}`, | ||||
|                     callBack: () => { | ||||
|                     onItemClicked: () => { | ||||
|                         return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                     } | ||||
|                 }); | ||||
| @@ -89,7 +89,7 @@ export default { | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: 'icon-camera', | ||||
|                 name: 'Save to Notebook Snapshots', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     return this.snapshot(NOTEBOOK_SNAPSHOT); | ||||
|                 } | ||||
|             }); | ||||
|   | ||||
| @@ -7,10 +7,10 @@ | ||||
|                     <div class="c-object-label__type-icon icon-camera"></div> | ||||
|                     <div class="c-object-label__name"> | ||||
|                         Notebook Snapshots | ||||
|                         <span v-if="snapshots.length" | ||||
|                               class="l-browse-bar__object-details" | ||||
|                         > {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} | ||||
|                         </span> | ||||
|                     </div> | ||||
|                     <div v-if="snapshots.length" | ||||
|                          class="l-browse-bar__object-details" | ||||
|                     >{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <PopupMenu v-if="snapshots.length > 0" | ||||
|   | ||||
| @@ -4,8 +4,10 @@ | ||||
|         <div class="l-browse-bar__start"> | ||||
|             <div class="l-browse-bar__object-name--w"> | ||||
|                 <span class="c-object-label l-browse-bar__object-name" | ||||
|                     v-bind:class="cssClass" | ||||
|                 > | ||||
|                     <span class="c-object-label__type-icon" | ||||
|                           v-bind:class="cssClass" | ||||
|                     ></span> | ||||
|                     <span class="c-object-label__name">{{ name }}</span> | ||||
|                 </span> | ||||
|             </div> | ||||
|   | ||||
| @@ -4,24 +4,24 @@ import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; | ||||
| import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; | ||||
|  | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| import ImageExporter from '../../exporters/ImageExporter'; | ||||
|  | ||||
| export default class Snapshot { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.snapshotContainer = new SnapshotContainer(openmct); | ||||
|         this.imageExporter = new ImageExporter(openmct); | ||||
|  | ||||
|         this.capture = this.capture.bind(this); | ||||
|         this._saveSnapShot = this._saveSnapShot.bind(this); | ||||
|     } | ||||
|  | ||||
|     capture(snapshotMeta, notebookType, domElement) { | ||||
|         const exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|  | ||||
|         const options = { | ||||
|             className: 's-status-taking-snapshot', | ||||
|             thumbnailSize: DEFAULT_SIZE | ||||
|         }; | ||||
|         exportImageService.exportPNGtoSRC(domElement, options) | ||||
|         this.imageExporter.exportPNGtoSRC(domElement, options) | ||||
|             .then(function ({blob, thumbnail}) { | ||||
|                 const reader = new window.FileReader(); | ||||
|                 reader.readAsDataURL(blob); | ||||
|   | ||||
							
								
								
									
										106
									
								
								src/plugins/persistence/couch/CouchChangesFeed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/plugins/persistence/couch/CouchChangesFeed.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| (function () { | ||||
|     const connections = []; | ||||
|     let connected = false; | ||||
|     const controller = new AbortController(); | ||||
|     const signal = controller.signal; | ||||
|  | ||||
|     self.onconnect = function (e) { | ||||
|         let port = e.ports[0]; | ||||
|         connections.push(port); | ||||
|  | ||||
|         port.postMessage({ | ||||
|             type: 'connection', | ||||
|             connectionId: connections.length | ||||
|         }); | ||||
|  | ||||
|         port.onmessage = async function (event) { | ||||
|             if (event.data.request === 'close') { | ||||
|                 connections.splice(event.data.connectionId - 1, 1); | ||||
|                 if (connections.length <= 0) { | ||||
|                     // abort any outstanding requests if there's nobody listening to it. | ||||
|                     controller.abort(); | ||||
|                 } | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (event.data.request === 'changes') { | ||||
|                 if (connected === true) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 connected = true; | ||||
|  | ||||
|                 let url = event.data.url; | ||||
|                 let body = event.data.body; | ||||
|                 let error = false; | ||||
|                 // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection | ||||
|                 // style=main_only returns only the current winning revision of the document | ||||
|  | ||||
|                 const response = await fetch(url, { | ||||
|                     method: 'POST', | ||||
|                     headers: { | ||||
|                         "Content-Type": 'application/json' | ||||
|                     }, | ||||
|                     signal, | ||||
|                     body | ||||
|                 }); | ||||
|  | ||||
|                 let reader; | ||||
|  | ||||
|                 if (response.body === undefined) { | ||||
|                     error = true; | ||||
|                 } else { | ||||
|                     reader = response.body.getReader(); | ||||
|                 } | ||||
|  | ||||
|                 while (!error) { | ||||
|                     const {done, value} = await reader.read(); | ||||
|                     //done is true when we lose connection with the provider | ||||
|                     if (done) { | ||||
|                         error = true; | ||||
|                     } | ||||
|  | ||||
|                     if (value) { | ||||
|                         let chunk = new Uint8Array(value.length); | ||||
|                         chunk.set(value, 0); | ||||
|                         const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n'); | ||||
|                         if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') { | ||||
|                             decodedChunk.forEach((doc, index) => { | ||||
|                                 try { | ||||
|                                     if (doc) { | ||||
|                                         const objectChanges = JSON.parse(doc); | ||||
|                                         connections.forEach(function (connection) { | ||||
|                                             connection.postMessage({ | ||||
|                                                 objectChanges | ||||
|                                             }); | ||||
|                                         }); | ||||
|                                     } | ||||
|                                 } catch (decodeError) { | ||||
|                                     //do nothing; | ||||
|                                     console.log(decodeError); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|                 if (error) { | ||||
|                     port.postMessage({ | ||||
|                         error | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         port.start(); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     self.onerror = function () { | ||||
|         //do nothing | ||||
|         console.log('Error on feed'); | ||||
|     }; | ||||
|  | ||||
| }()); | ||||
| @@ -40,6 +40,64 @@ export default class CouchObjectProvider { | ||||
|         this.batchIds = []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     startSharedWorker() { | ||||
|         let provider = this; | ||||
|         let sharedWorker; | ||||
|  | ||||
|         const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`; | ||||
|  | ||||
|         sharedWorker = new SharedWorker(sharedWorkerURL); | ||||
|         sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this); | ||||
|         sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this); | ||||
|         sharedWorker.port.start(); | ||||
|  | ||||
|         this.openmct.on('destroy', () => { | ||||
|             this.changesFeedSharedWorker.port.postMessage({ | ||||
|                 request: 'close', | ||||
|                 connectionId: this.changesFeedSharedWorkerConnectionId | ||||
|             }); | ||||
|             this.changesFeedSharedWorker.port.close(); | ||||
|         }); | ||||
|  | ||||
|         return sharedWorker; | ||||
|     } | ||||
|  | ||||
|     onSharedWorkerMessageError(event) { | ||||
|         console.log('Error', event); | ||||
|     } | ||||
|  | ||||
|     onSharedWorkerMessage(event) { | ||||
|         if (event.data.type === 'connection') { | ||||
|             this.changesFeedSharedWorkerConnectionId = event.data.connectionId; | ||||
|         } else { | ||||
|             const error = event.data.error; | ||||
|             if (error && Object.keys(this.observers).length > 0) { | ||||
|                 this.observeObjectChanges(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let objectChanges = event.data.objectChanges; | ||||
|             objectChanges.identifier = { | ||||
|                 namespace: this.namespace, | ||||
|                 key: objectChanges.id | ||||
|             }; | ||||
|             let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier); | ||||
|             //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. | ||||
|             let observersForObject = this.observers[keyString]; | ||||
|  | ||||
|             if (observersForObject) { | ||||
|                 observersForObject.forEach(async (observer) => { | ||||
|                     const updatedObject = await this.get(objectChanges.identifier); | ||||
|                     observer(updatedObject); | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     //backwards compatibility, options used to be a url. Now it's an object | ||||
|     _normalize(options) { | ||||
|         if (typeof options === 'string') { | ||||
| @@ -320,7 +378,7 @@ export default class CouchObjectProvider { | ||||
|             this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback); | ||||
|             if (this.observers[keyString].length === 0) { | ||||
|                 delete this.observers[keyString]; | ||||
|                 if (Object.keys(this.observers).length === 0) { | ||||
|                 if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) { | ||||
|                     this.stopObservingObjectChanges(); | ||||
|                 } | ||||
|             } | ||||
| @@ -334,9 +392,8 @@ export default class CouchObjectProvider { | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     async observeObjectChanges() { | ||||
|         const controller = new AbortController(); | ||||
|         const signal = controller.signal; | ||||
|     observeObjectChanges() { | ||||
|  | ||||
|         let filter = {selector: {}}; | ||||
|  | ||||
|         if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) { | ||||
| @@ -354,17 +411,6 @@ export default class CouchObjectProvider { | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         let error = false; | ||||
|  | ||||
|         if (typeof this.stopObservingObjectChanges === 'function') { | ||||
|             this.stopObservingObjectChanges(); | ||||
|         } | ||||
|  | ||||
|         this.stopObservingObjectChanges = () => { | ||||
|             controller.abort(); | ||||
|             delete this.stopObservingObjectChanges; | ||||
|         }; | ||||
|  | ||||
|         // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection | ||||
|         // style=main_only returns only the current winning revision of the document | ||||
|         let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`; | ||||
| @@ -375,6 +421,52 @@ export default class CouchObjectProvider { | ||||
|             body = JSON.stringify(filter); | ||||
|         } | ||||
|  | ||||
|         if (typeof SharedWorker === 'undefined') { | ||||
|             this.fetchChanges(url, body); | ||||
|         } else { | ||||
|             this.initiateSharedWorkerFetchChanges(url, body); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     initiateSharedWorkerFetchChanges(url, body) { | ||||
|         if (!this.changesFeedSharedWorker) { | ||||
|             this.changesFeedSharedWorker = this.startSharedWorker(); | ||||
|  | ||||
|             if (this.isObservingObjectChanges()) { | ||||
|                 this.stopObservingObjectChanges(); | ||||
|             } | ||||
|  | ||||
|             this.stopObservingObjectChanges = () => { | ||||
|                 delete this.stopObservingObjectChanges; | ||||
|             }; | ||||
|  | ||||
|             this.changesFeedSharedWorker.port.postMessage({ | ||||
|                 request: 'changes', | ||||
|                 body, | ||||
|                 url | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fetchChanges(url, body) { | ||||
|         const controller = new AbortController(); | ||||
|         const signal = controller.signal; | ||||
|  | ||||
|         let error = false; | ||||
|  | ||||
|         if (this.isObservingObjectChanges()) { | ||||
|             this.stopObservingObjectChanges(); | ||||
|         } | ||||
|  | ||||
|         this.stopObservingObjectChanges = () => { | ||||
|             controller.abort(); | ||||
|             delete this.stopObservingObjectChanges; | ||||
|         }; | ||||
|  | ||||
|         const response = await fetch(url, { | ||||
|             method: 'POST', | ||||
|             signal, | ||||
|   | ||||
| @@ -49,10 +49,6 @@ describe('the plugin', function () { | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 1597160002854, | ||||
|             end: 1597181232854 | ||||
|         }); | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
| @@ -105,6 +101,11 @@ describe('the plugin', function () { | ||||
|         let planView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: 1597160002854, | ||||
|                 end: 1597181232854 | ||||
|             }); | ||||
|  | ||||
|             planDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'test-object', | ||||
|   | ||||
| @@ -25,16 +25,16 @@ | ||||
|      :class="[plotLegendExpandedStateClass, plotLegendPositionClass]" | ||||
| > | ||||
|     <plot-legend :cursor-locked="!!lockHighlightPoint" | ||||
|                  :series="config.series.models" | ||||
|                  :series="seriesModels" | ||||
|                  :highlights="highlights" | ||||
|                  :legend="config.legend" | ||||
|                  :legend="legend" | ||||
|                  @legendHoverChanged="legendHoverChanged" | ||||
|     /> | ||||
|     <div class="plot-wrapper-axis-and-display-area flex-elem grows"> | ||||
|         <y-axis v-if="config.series.models.length > 0" | ||||
|         <y-axis v-if="seriesModels.length > 0" | ||||
|                 :tick-width="tickWidth" | ||||
|                 :single-series="config.series.models.length === 1" | ||||
|                 :series-model="config.series.models[0]" | ||||
|                 :single-series="seriesModels.length === 1" | ||||
|                 :series-model="seriesModels[0]" | ||||
|                 @yKeyChanged="setYAxisKey" | ||||
|                 @tickWidthChanged="onTickWidthChange" | ||||
|         /> | ||||
| @@ -141,8 +141,8 @@ | ||||
|                 > | ||||
|                 </div> | ||||
|             </div> | ||||
|             <x-axis v-if="config.series.models.length > 0 && !options.compact" | ||||
|                     :series-model="config.series.models[0]" | ||||
|             <x-axis v-if="seriesModels.length > 0 && !options.compact" | ||||
|                     :series-model="seriesModels[0]" | ||||
|             /> | ||||
|  | ||||
|         </div> | ||||
| @@ -213,7 +213,8 @@ export default { | ||||
|             plotHistory: [], | ||||
|             selectedXKeyOption: {}, | ||||
|             xKeyOptions: [], | ||||
|             config: {}, | ||||
|             seriesModels: [], | ||||
|             legend: {}, | ||||
|             pending: 0, | ||||
|             isRealTime: this.openmct.time.clock() !== undefined, | ||||
|             loaded: false, | ||||
| @@ -239,18 +240,13 @@ export default { | ||||
|     watch: { | ||||
|         plotTickWidth(newTickWidth) { | ||||
|             this.onTickWidthChange(newTickWidth, true); | ||||
|         }, | ||||
|         gridLines(newGridLines) { | ||||
|             this.setGridLinesVisibility(newGridLines); | ||||
|         }, | ||||
|         cursorGuide(newCursorGuide) { | ||||
|             this.setCursorGuideVisibility(newCursorGuide); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|  | ||||
|         this.config = this.getConfig(); | ||||
|         this.legend = this.config.legend; | ||||
|  | ||||
|         this.listenTo(this.config.series, 'add', this.addSeries, this); | ||||
|         this.listenTo(this.config.series, 'remove', this.removeSeries, this); | ||||
| @@ -290,14 +286,18 @@ export default { | ||||
|                 config = new PlotConfigurationModel({ | ||||
|                     id: configId, | ||||
|                     domainObject: this.domainObject, | ||||
|                     openmct: this.openmct | ||||
|                     openmct: this.openmct, | ||||
|                     callback: (data) => { | ||||
|                         this.data = data; | ||||
|                     } | ||||
|                 }); | ||||
|                 configStore.add(configId, config); | ||||
|             } | ||||
|  | ||||
|             return config; | ||||
|         }, | ||||
|         addSeries(series) { | ||||
|         addSeries(series, index) { | ||||
|             this.$set(this.seriesModels, index, series); | ||||
|             this.listenTo(series, 'change:xKey', (xKey) => { | ||||
|                 this.setDisplayRange(series, xKey); | ||||
|             }, this); | ||||
| @@ -377,11 +377,8 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         stopLoading() { | ||||
|         //TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync? | ||||
|             this.$nextTick().then(() => { | ||||
|                 this.pending -= 1; | ||||
|                 this.updateLoading(); | ||||
|             }); | ||||
|             this.pending -= 1; | ||||
|             this.updateLoading(); | ||||
|         }, | ||||
|  | ||||
|         updateLoading() { | ||||
| @@ -427,9 +424,12 @@ export default { | ||||
|                 this.skipReloadOnInteraction = false; | ||||
|                 this.loadMoreData(newRange, true); | ||||
|             } else { | ||||
|                 // If we're not panning or zooming (time conductor and plot x-axis times are not out of sync) | ||||
|                 // Drop any data that is more than 1x (max-min) before min. | ||||
|                 // Limit these purges to once a second. | ||||
|                 if (!this.nextPurge || this.nextPurge < Date.now()) { | ||||
|                 const isPanningOrZooming = this.isTimeOutOfSync; | ||||
|                 const purgeRecords = !isPanningOrZooming && (!this.nextPurge || (this.nextPurge < Date.now())); | ||||
|                 if (purgeRecords) { | ||||
|                     const keepRange = { | ||||
|                         min: newRange.min - (newRange.max - newRange.min), | ||||
|                         max: newRange.max | ||||
| @@ -504,7 +504,7 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         initialize() { | ||||
|             _.debounce(this.handleWindowResize, 400); | ||||
|             this.handleWindowResize = _.debounce(this.handleWindowResize, 500); | ||||
|             this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize); | ||||
|             this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper); | ||||
|  | ||||
| @@ -620,7 +620,7 @@ export default { | ||||
|                 this.config.series.models.forEach(series => delete series.closest); | ||||
|             } else { | ||||
|                 this.highlights = this.config.series.models | ||||
|                     .filter(series => series.data.length > 0) | ||||
|                     .filter(series => series.getSeriesData().length > 0) | ||||
|                     .map(series => { | ||||
|                         series.closest = series.nearestPoint(point); | ||||
|  | ||||
| @@ -924,16 +924,8 @@ export default { | ||||
|             this.userViewportChangeEnd(); | ||||
|         }, | ||||
|  | ||||
|         setCursorGuideVisibility(cursorGuide) { | ||||
|             this.cursorGuide = cursorGuide === true; | ||||
|         }, | ||||
|  | ||||
|         setGridLinesVisibility(gridLines) { | ||||
|             this.gridLines = gridLines === true; | ||||
|         }, | ||||
|  | ||||
|         setYAxisKey(yKey) { | ||||
|             this.config.series.models[0].emit('change:yKey', yKey); | ||||
|             this.config.series.models[0].set('yKey', yKey); | ||||
|         }, | ||||
|  | ||||
|         pause() { | ||||
|   | ||||
| @@ -72,7 +72,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import eventHelpers from "./lib/eventHelpers"; | ||||
| import eventHelpers from './lib/eventHelpers'; | ||||
| import ImageExporter from '../../exporters/ImageExporter'; | ||||
| import MctPlot from './MctPlot.vue'; | ||||
|  | ||||
| export default { | ||||
| @@ -102,8 +103,7 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|  | ||||
|         this.exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.destroy(); | ||||
| @@ -118,14 +118,12 @@ export default { | ||||
|  | ||||
|         exportJPG() { | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|  | ||||
|             this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot'); | ||||
|             this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot'); | ||||
|         }, | ||||
|  | ||||
|         exportPNG() { | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|  | ||||
|             this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot'); | ||||
|             this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot'); | ||||
|         }, | ||||
|  | ||||
|         toggleCursorGuide() { | ||||
|   | ||||
| @@ -24,19 +24,23 @@ import Plot from './Plot.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function PlotViewProvider(openmct) { | ||||
|     function hasTelemetry(domainObject) { | ||||
|     function hasNumericTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasDomainAndRange(metadata); | ||||
|         return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); | ||||
|     } | ||||
|  | ||||
|     function hasDomainAndRange(metadata) { | ||||
|         return (metadata.valuesForHints(['range']).length > 0 | ||||
|             && metadata.valuesForHints(['domain']).length > 0); | ||||
|     function hasDomainAndNumericRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
|         const domains = metadata.valuesForHints(['domain']); | ||||
|  | ||||
|         return domains.length > 0 | ||||
|             && rangeValues.length > 0 | ||||
|             && !rangeValues.every(value => value.format === 'string'); | ||||
|     } | ||||
|  | ||||
|     function isCompactView(objectPath) { | ||||
| @@ -44,11 +48,11 @@ export default function PlotViewProvider(openmct) { | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: 'plot-simple', | ||||
|         key: 'plot-single', | ||||
|         name: 'Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return hasTelemetry(domainObject, openmct); | ||||
|             return hasNumericTelemetry(domainObject); | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|   | ||||
| @@ -106,8 +106,9 @@ export default { | ||||
|         }, | ||||
|         toggleXKeyOption() { | ||||
|             const selectedXKey = this.selectedXKeyOptionKey; | ||||
|             const dataForSelectedXKey = this.seriesModel.data | ||||
|                 ? this.seriesModel.data[0][selectedXKey] | ||||
|             const seriesData = this.seriesModel.getSeriesData(); | ||||
|             const dataForSelectedXKey = seriesData | ||||
|                 ? seriesData[0][selectedXKey] | ||||
|                 : undefined; | ||||
|  | ||||
|             if (dataForSelectedXKey !== undefined) { | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export default class MCTChartAlarmPointSet { | ||||
|         this.listenTo(series, 'reset', this.reset, this); | ||||
|         this.listenTo(series, 'destroy', this.destroy, this); | ||||
|  | ||||
|         series.data.forEach(function (point, index) { | ||||
|         this.series.getSeriesData().forEach(function (point, index) { | ||||
|             this.append(point, index, series); | ||||
|         }, this); | ||||
|     } | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export default class MCTChartSeriesElement { | ||||
|         this.listenTo(series, 'remove', this.remove, this); | ||||
|         this.listenTo(series, 'reset', this.reset, this); | ||||
|         this.listenTo(series, 'destroy', this.destroy, this); | ||||
|         series.data.forEach(function (point, index) { | ||||
|         this.series.getSeriesData().forEach(function (point, index) { | ||||
|             this.append(point, index, series); | ||||
|         }, this); | ||||
|     } | ||||
| @@ -133,7 +133,7 @@ export default class MCTChartSeriesElement { | ||||
|         this.buffer = new Float32Array(20000); | ||||
|         this.count = 0; | ||||
|         if (this.offset.x) { | ||||
|             this.series.data.forEach(function (point, index) { | ||||
|             this.series.getSeriesData().forEach(function (point, index) { | ||||
|                 this.append(point, index, this.series); | ||||
|             }, this); | ||||
|         } | ||||
|   | ||||
| @@ -107,6 +107,7 @@ export default class PlotConfigurationModel extends Model { | ||||
|     updateDomainObject(domainObject) { | ||||
|         this.set('domainObject', domainObject); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clean up all objects and remove all listeners. | ||||
|      */ | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
| import _ from 'lodash'; | ||||
| import Model from "./Model"; | ||||
| import { MARKER_SHAPES } from '../draw/MarkerShapes'; | ||||
| import configStore from "../configuration/configStore"; | ||||
|  | ||||
| /** | ||||
|  * Plot series handle interpreting telemetry metadata for a single telemetry | ||||
| @@ -62,7 +63,6 @@ import { MARKER_SHAPES } from '../draw/MarkerShapes'; | ||||
| export default class PlotSeries extends Model { | ||||
|     constructor(options) { | ||||
|         super(options); | ||||
|         this.data = []; | ||||
|  | ||||
|         this.listenTo(this, 'change:xKey', this.onXKeyChange, this); | ||||
|         this.listenTo(this, 'change:yKey', this.onYKeyChange, this); | ||||
| @@ -115,6 +115,8 @@ export default class PlotSeries extends Model { | ||||
|         this.openmct = options.openmct; | ||||
|         this.domainObject = options.domainObject; | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`; | ||||
|         this.updateSeriesData([]); | ||||
|         this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); | ||||
|         this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); | ||||
|         this.limits = []; | ||||
| @@ -182,7 +184,8 @@ export default class PlotSeries extends Model { | ||||
|             .telemetry | ||||
|             .request(this.domainObject, options) | ||||
|             .then(function (points) { | ||||
|                 const newPoints = _(this.data) | ||||
|                 const data = this.getSeriesData(); | ||||
|                 const newPoints = _(data) | ||||
|                     .concat(points) | ||||
|                     .sortBy(this.getXVal) | ||||
|                     .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) | ||||
| @@ -236,7 +239,7 @@ export default class PlotSeries extends Model { | ||||
|      */ | ||||
|     resetStats() { | ||||
|         this.unset('stats'); | ||||
|         this.data.forEach(this.updateStats, this); | ||||
|         this.getSeriesData().forEach(this.updateStats, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -244,7 +247,7 @@ export default class PlotSeries extends Model { | ||||
|      * data to series after reset. | ||||
|      */ | ||||
|     reset(newData) { | ||||
|         this.data = []; | ||||
|         this.updateSeriesData([]); | ||||
|         this.resetStats(); | ||||
|         this.emit('reset'); | ||||
|         if (newData) { | ||||
| @@ -258,8 +261,9 @@ export default class PlotSeries extends Model { | ||||
|      */ | ||||
|     nearestPoint(xValue) { | ||||
|         const insertIndex = this.sortedIndex(xValue); | ||||
|         const lowPoint = this.data[insertIndex - 1]; | ||||
|         const highPoint = this.data[insertIndex]; | ||||
|         const data = this.getSeriesData(); | ||||
|         const lowPoint = data[insertIndex - 1]; | ||||
|         const highPoint = data[insertIndex]; | ||||
|         const indexVal = this.getXVal(xValue); | ||||
|         const lowDistance = lowPoint | ||||
|             ? indexVal - this.getXVal(lowPoint) | ||||
| @@ -292,7 +296,7 @@ export default class PlotSeries extends Model { | ||||
|      * @private | ||||
|      */ | ||||
|     sortedIndex(point) { | ||||
|         return _.sortedIndexBy(this.data, point, this.getXVal); | ||||
|         return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal); | ||||
|     } | ||||
|     /** | ||||
|      * Update min/max stats for the series. | ||||
| @@ -346,9 +350,10 @@ export default class PlotSeries extends Model { | ||||
|      *                  a point to the end without dupe checking. | ||||
|      */ | ||||
|     add(point, appendOnly) { | ||||
|         let insertIndex = this.data.length; | ||||
|         let data = this.getSeriesData(); | ||||
|         let insertIndex = data.length; | ||||
|         const currentYVal = this.getYVal(point); | ||||
|         const lastYVal = this.getYVal(this.data[insertIndex - 1]); | ||||
|         const lastYVal = this.getYVal(data[insertIndex - 1]); | ||||
|  | ||||
|         if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) { | ||||
|             console.warn('[Plot] Invalid Y Values detected'); | ||||
| @@ -358,18 +363,19 @@ export default class PlotSeries extends Model { | ||||
|  | ||||
|         if (!appendOnly) { | ||||
|             insertIndex = this.sortedIndex(point); | ||||
|             if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) { | ||||
|             if (this.getXVal(data[insertIndex]) === this.getXVal(point)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) { | ||||
|             if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) { | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.updateStats(point); | ||||
|         point.mctLimitState = this.evaluate(point); | ||||
|         this.data.splice(insertIndex, 0, point); | ||||
|         data.splice(insertIndex, 0, point); | ||||
|         this.updateSeriesData(data); | ||||
|         this.emit('add', point, insertIndex, this); | ||||
|     } | ||||
|  | ||||
| @@ -386,8 +392,10 @@ export default class PlotSeries extends Model { | ||||
|      * @private | ||||
|      */ | ||||
|     remove(point) { | ||||
|         const index = this.data.indexOf(point); | ||||
|         this.data.splice(index, 1); | ||||
|         let data = this.getSeriesData(); | ||||
|         const index = data.indexOf(point); | ||||
|         data.splice(index, 1); | ||||
|         this.updateSeriesData(data); | ||||
|         this.emit('remove', point, index, this); | ||||
|     } | ||||
|     /** | ||||
| @@ -403,14 +411,16 @@ export default class PlotSeries extends Model { | ||||
|     purgeRecordsOutsideRange(range) { | ||||
|         const startIndex = this.sortedIndex(range.min); | ||||
|         const endIndex = this.sortedIndex(range.max) + 1; | ||||
|         const pointsToRemove = startIndex + (this.data.length - endIndex + 1); | ||||
|         let data = this.getSeriesData(); | ||||
|         const pointsToRemove = startIndex + (data.length - endIndex + 1); | ||||
|         if (pointsToRemove > 0) { | ||||
|             if (pointsToRemove < 1000) { | ||||
|                 this.data.slice(0, startIndex).forEach(this.remove, this); | ||||
|                 this.data.slice(endIndex, this.data.length).forEach(this.remove, this); | ||||
|                 data.slice(0, startIndex).forEach(this.remove, this); | ||||
|                 data.slice(endIndex, data.length).forEach(this.remove, this); | ||||
|                 this.updateSeriesData(data); | ||||
|                 this.resetStats(); | ||||
|             } else { | ||||
|                 const newData = this.data.slice(startIndex, endIndex); | ||||
|                 const newData = this.getSeriesData().slice(startIndex, endIndex); | ||||
|                 this.reset(newData); | ||||
|             } | ||||
|         } | ||||
| @@ -441,12 +451,13 @@ export default class PlotSeries extends Model { | ||||
|         } | ||||
|     } | ||||
|     getDisplayRange(xKey) { | ||||
|         const unsortedData = this.data; | ||||
|         this.data = []; | ||||
|         const unsortedData = this.getSeriesData(); | ||||
|         this.updateSeriesData([]); | ||||
|         unsortedData.forEach(point => this.add(point, false)); | ||||
|  | ||||
|         const minValue = this.getXVal(this.data[0]); | ||||
|         const maxValue = this.getXVal(this.data[this.data.length - 1]); | ||||
|         let data = this.getSeriesData(); | ||||
|         const minValue = this.getXVal(data[0]); | ||||
|         const maxValue = this.getXVal(data[data.length - 1]); | ||||
|  | ||||
|         return { | ||||
|             min: minValue, | ||||
| @@ -470,4 +481,18 @@ export default class PlotSeries extends Model { | ||||
|  | ||||
|         return this.get('name') + (unit ? ' ' + unit : ''); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update the series data with the given value. | ||||
|      */ | ||||
|     updateSeriesData(data) { | ||||
|         configStore.add(this.dataStoreId, data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update the series data with the given value. | ||||
|      */ | ||||
|     getSeriesData() { | ||||
|         return configStore.get(this.dataStoreId) || []; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,10 @@ function ConfigStore() { | ||||
|  | ||||
| ConfigStore.prototype.deleteStore = function (id) { | ||||
|     if (this.store[id]) { | ||||
|         this.store[id].destroy(); | ||||
|         if (this.store[id].destroy) { | ||||
|             this.store[id].destroy(); | ||||
|         } | ||||
|  | ||||
|         delete this.store[id]; | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -176,7 +176,9 @@ DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) { | ||||
|     this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); | ||||
|     this.gl.uniform4fv(this.uColor, color); | ||||
|     this.gl.uniform1i(this.uMarkerShape, shapeCode); | ||||
|     this.gl.drawArrays(drawType, 0, points); | ||||
|     if (points !== 0) { | ||||
|         this.gl.drawArrays(drawType, 0, points); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| DrawWebGL.prototype.clear = function () { | ||||
|   | ||||
| @@ -201,15 +201,57 @@ describe("the plugin", function () { | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "yet-another-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); | ||||
|             const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|  | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("does not provide a plot view if the telemetry is entirely non numeric", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "test-object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "other-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "yet-another-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|  | ||||
|             expect(plotView).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         it("provides an overlay plot view for objects with telemetry", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
| @@ -279,14 +321,10 @@ describe("the plugin", function () { | ||||
|         let plotView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             const getFunc = openmct.$injector.get; | ||||
|             spyOn(openmct.$injector, "get") | ||||
|                 .withArgs("exportImageService").and.returnValue({ | ||||
|                     exportPNG: () => {}, | ||||
|                     exportJPG: () => {} | ||||
|                 }) | ||||
|                 .and.callFake(getFunc); | ||||
|  | ||||
|             openmct.time.timeSystem("utc", { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             }); | ||||
|             testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
| @@ -319,7 +357,7 @@ describe("the plugin", function () { | ||||
|             }; | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); | ||||
|             plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|             plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]); | ||||
|             plotView.show(child, true); | ||||
|  | ||||
| @@ -567,7 +605,7 @@ describe("the plugin", function () { | ||||
|             expect(legend.length).toBe(6); | ||||
|         }); | ||||
|  | ||||
|         it("Renders X-axis ticks for the telemetry object", () => { | ||||
|         xit("Renders X-axis ticks for the telemetry object", () => { | ||||
|             let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper"); | ||||
|             expect(xAxisElement.length).toBe(1); | ||||
|  | ||||
| @@ -677,14 +715,15 @@ describe("the plugin", function () { | ||||
|         }); | ||||
|  | ||||
|         it("Adds a new point to the plot", (done) => { | ||||
|             let originalLength = config.series.models[0].data.length; | ||||
|             let originalLength = config.series.models[0].getSeriesData().length; | ||||
|             config.series.models[0].add({ | ||||
|                 utc: 2, | ||||
|                 'some-key': 1, | ||||
|                 'some-other-key': 2 | ||||
|             }); | ||||
|             Vue.nextTick(() => { | ||||
|                 expect(config.series.models[0].data.length).toEqual(originalLength + 1); | ||||
|                 const seriesData = config.series.models[0].getSeriesData(); | ||||
|                 expect(seriesData.length).toEqual(originalLength + 1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -67,8 +67,9 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import eventHelpers from "../lib/eventHelpers"; | ||||
| import StackedPlotItem from "./StackedPlotItem.vue"; | ||||
| import eventHelpers from '../lib/eventHelpers'; | ||||
| import StackedPlotItem from './StackedPlotItem.vue'; | ||||
| import ImageExporter from '../../../exporters/ImageExporter'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -103,7 +104,7 @@ export default { | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|  | ||||
|         this.exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|  | ||||
|         this.tickWidthMap = {}; | ||||
|  | ||||
| @@ -159,9 +160,9 @@ export default { | ||||
|  | ||||
|         exportJPG() { | ||||
|             this.hideExportButtons = true; | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|             const plotElement = this.$el; | ||||
|  | ||||
|             this.exportImageService.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot') | ||||
|             this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot') | ||||
|                 .finally(function () { | ||||
|                     this.hideExportButtons = false; | ||||
|                 }.bind(this)); | ||||
| @@ -170,9 +171,9 @@ export default { | ||||
|         exportPNG() { | ||||
|             this.hideExportButtons = true; | ||||
|  | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|             const plotElement = this.$el; | ||||
|  | ||||
|             this.exportImageService.exportPNG(plotElement, 'stacked-plot.png', 'export-plot') | ||||
|             this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot') | ||||
|                 .finally(function () { | ||||
|                     this.hideExportButtons = false; | ||||
|                 }.bind(this)); | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| define([ | ||||
|     'lodash', | ||||
|     './utcTimeSystem/plugin', | ||||
|     './remoteClock/plugin', | ||||
|     './localTimeSystem/plugin', | ||||
|     './ISOTimeFormat/plugin', | ||||
|     '../../example/generator/plugin', | ||||
| @@ -62,14 +63,17 @@ define([ | ||||
|     './defaultRootName/plugin', | ||||
|     './plan/plugin', | ||||
|     './viewDatumAction/plugin', | ||||
|     './viewLargeAction/plugin', | ||||
|     './interceptors/plugin', | ||||
|     './performanceIndicator/plugin', | ||||
|     './CouchDBSearchFolder/plugin', | ||||
|     './timeline/plugin', | ||||
|     '/node_modules/openmct-mmgis/dist/openmct-mmgis' | ||||
|     './hyperlink/plugin', | ||||
|     './clock/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
|     UTCTimeSystem, | ||||
|     RemoteClock, | ||||
|     LocalTimeSystem, | ||||
|     ISOTimeFormat, | ||||
|     GeneratorPlugin, | ||||
| @@ -109,11 +113,13 @@ define([ | ||||
|     DefaultRootName, | ||||
|     PlanLayout, | ||||
|     ViewDatumAction, | ||||
|     ViewLargeAction, | ||||
|     ObjectInterceptors, | ||||
|     PerformanceIndicator, | ||||
|     CouchDBSearchFolder, | ||||
|     Timeline, | ||||
|     Mmgis | ||||
|     Hyperlink, | ||||
|     Clock | ||||
| ) { | ||||
|     const bundleMap = { | ||||
|         LocalStorage: 'platform/persistence/local', | ||||
| @@ -131,6 +137,7 @@ define([ | ||||
|  | ||||
|     plugins.UTCTimeSystem = UTCTimeSystem; | ||||
|     plugins.LocalTimeSystem = LocalTimeSystem; | ||||
|     plugins.RemoteClock = RemoteClock.default; | ||||
|  | ||||
|     plugins.ImportExport = ImportExport; | ||||
|  | ||||
| @@ -210,11 +217,13 @@ define([ | ||||
|     plugins.DefaultRootName = DefaultRootName.default; | ||||
|     plugins.PlanLayout = PlanLayout.default; | ||||
|     plugins.ViewDatumAction = ViewDatumAction.default; | ||||
|     plugins.ViewLargeAction = ViewLargeAction.default; | ||||
|     plugins.ObjectInterceptors = ObjectInterceptors.default; | ||||
|     plugins.PerformanceIndicator = PerformanceIndicator.default; | ||||
|     plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; | ||||
|     plugins.Timeline = Timeline.default; | ||||
|     plugins.Mmgis = Mmgis.default; | ||||
|     plugins.Hyperlink = Hyperlink.default; | ||||
|     plugins.Clock = Clock.default; | ||||
|  | ||||
|     return plugins; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										132
									
								
								src/plugins/remoteClock/RemoteClock.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/plugins/remoteClock/RemoteClock.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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 DefaultClock from '../../utils/clock/DefaultClock'; | ||||
|  | ||||
| /** | ||||
|  * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the | ||||
|  * application based on a time providing telemetry domainObject. | ||||
|  * | ||||
|  * @param {openmct} Object Instance of OpenMCT | ||||
|  * @param {module:openmct.ObjectAPI~Identifier} identifier An object identifier for | ||||
|  * the time providing telemetry domainObject | ||||
|  * @constructor | ||||
|  */ | ||||
|  | ||||
| export default class RemoteClock extends DefaultClock { | ||||
|     constructor(openmct, identifier) { | ||||
|         super(); | ||||
|  | ||||
|         this.key = 'remote-clock'; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.identifier = identifier; | ||||
|  | ||||
|         this.name = 'Remote Clock'; | ||||
|         this.description = "Provides telemetry based timestamps from a configurable source."; | ||||
|  | ||||
|         this.timeTelemetryObject = undefined; | ||||
|         this.parseTime = undefined; | ||||
|         this.metadata = undefined; | ||||
|  | ||||
|         this.lastTick = 0; | ||||
|  | ||||
|         this._processDatum = this._processDatum.bind(this); | ||||
|     } | ||||
|  | ||||
|     start() { | ||||
|         this.openmct.time.on('timeSystem', this._timeSystemChange); | ||||
|         this.openmct.objects.get(this.identifier).then((domainObject) => { | ||||
|             this.timeTelemetryObject = domainObject; | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
|             this._timeSystemChange(); | ||||
|             this._requestLatest(); | ||||
|             this._subscribe(); | ||||
|         }).catch((error) => { | ||||
|             throw new Error(error); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     stop() { | ||||
|         this.openmct.time.off('timeSystem', this._timeSystemChange); | ||||
|         if (this._unsubscribe) { | ||||
|             this._unsubscribe(); | ||||
|         } | ||||
|  | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will start a subscription to the timeTelemetryObject as well | ||||
|      * handle the unsubscribe callback | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     _subscribe() { | ||||
|         this._unsubscribe = this.openmct.telemetry.subscribe( | ||||
|             this.timeTelemetryObject, | ||||
|             this._processDatum | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will request the latest data for the timeTelemetryObject | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     _requestLatest() { | ||||
|         this.openmct.telemetry.request(this.timeTelemetryObject, { | ||||
|             size: 1, | ||||
|             strategy: 'latest' | ||||
|         }).then(data => { | ||||
|             this._processDatum(data[data.length - 1]); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Function to parse the datum from the timeTelemetryObject as well | ||||
|      * as check if it's valid, calls "tick" | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     _processDatum(datum) { | ||||
|         let time = this.parseTime(datum); | ||||
|  | ||||
|         if (time > this.lastTick) { | ||||
|             this.tick(time); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Callback function for timeSystem change events | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     _timeSystemChange() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         let timeKey = timeSystem.key; | ||||
|         let metadataValue = this.metadata.value(timeKey); | ||||
|         let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|         this.parseTime = (datum) => { | ||||
|             return timeFormatter.parse(datum); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										152
									
								
								src/plugins/remoteClock/RemoteClockSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/plugins/remoteClock/RemoteClockSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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'; | ||||
|  | ||||
| const REMOTE_CLOCK_KEY = 'remote-clock'; | ||||
| const TIME_TELEMETRY_ID = { | ||||
|     namespace: 'remote', | ||||
|     key: 'telemetry' | ||||
| }; | ||||
| const TIME_VALUE = 12345; | ||||
| const REQ_OPTIONS = { | ||||
|     size: 1, | ||||
|     strategy: 'latest' | ||||
| }; | ||||
| const OFFSET_START = -10; | ||||
| const OFFSET_END = 1; | ||||
|  | ||||
| describe("the RemoteClock plugin", () => { | ||||
|     let openmct; | ||||
|     let object = { | ||||
|         name: 'remote-telemetry', | ||||
|         identifier: TIME_TELEMETRY_ID | ||||
|     }; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('once installed', () => { | ||||
|         let remoteClock; | ||||
|         let boundsCallback; | ||||
|         let metadataValue = { some: 'value' }; | ||||
|         let timeSystem = { key: 'utc' }; | ||||
|         let metadata = { | ||||
|             value: () => metadataValue | ||||
|         }; | ||||
|         let reqDatum = { | ||||
|             key: TIME_VALUE | ||||
|         }; | ||||
|  | ||||
|         let formatter = { | ||||
|             parse: (datum) => datum.key | ||||
|         }; | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); | ||||
|  | ||||
|             let clocks = openmct.time.getAllClocks(); | ||||
|             remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0]; | ||||
|  | ||||
|             boundsCallback = jasmine.createSpy("boundsCallback"); | ||||
|             openmct.time.on('bounds', boundsCallback); | ||||
|  | ||||
|             spyOn(remoteClock, '_timeSystemChange').and.callThrough(); | ||||
|             spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); | ||||
|             spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); | ||||
|             spyOn(openmct.telemetry, 'subscribe').and.callThrough(); | ||||
|             spyOn(openmct.time, 'on').and.callThrough(); | ||||
|             spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); | ||||
|             spyOn(metadata, 'value').and.callThrough(); | ||||
|  | ||||
|             let requestPromiseResolve; | ||||
|             let requestPromise = new Promise((resolve) => { | ||||
|                 requestPromiseResolve = resolve; | ||||
|             }); | ||||
|             spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|                 requestPromiseResolve([reqDatum]); | ||||
|  | ||||
|                 return requestPromise; | ||||
|             }); | ||||
|  | ||||
|             let objectPromiseResolve; | ||||
|             let objectPromise = new Promise((resolve) => { | ||||
|                 objectPromiseResolve = resolve; | ||||
|             }); | ||||
|             spyOn(openmct.objects, 'get').and.callFake(() => { | ||||
|                 objectPromiseResolve(object); | ||||
|  | ||||
|                 return objectPromise; | ||||
|             }); | ||||
|  | ||||
|             openmct.time.clock(REMOTE_CLOCK_KEY, { | ||||
|                 start: OFFSET_START, | ||||
|                 end: OFFSET_END | ||||
|             }); | ||||
|  | ||||
|             Promise.all([objectPromiseResolve, requestPromise]) | ||||
|                 .then(done) | ||||
|                 .catch(done); | ||||
|         }); | ||||
|  | ||||
|         it('is available and sets up initial values and listeners', () => { | ||||
|             expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); | ||||
|             expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); | ||||
|             expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); | ||||
|             expect(remoteClock._timeSystemChange).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('will request/store the object based on the identifier passed in', () => { | ||||
|             expect(remoteClock.timeTelemetryObject).toEqual(object); | ||||
|         }); | ||||
|  | ||||
|         it('will request metadata and set up formatters', () => { | ||||
|             expect(remoteClock.metadata).toEqual(metadata); | ||||
|             expect(metadata.value).toHaveBeenCalled(); | ||||
|             expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); | ||||
|         }); | ||||
|  | ||||
|         it('will request the latest datum for the object it received and process the datum returned', () => { | ||||
|             expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS); | ||||
|             expect(boundsCallback).toHaveBeenCalledWith({ | ||||
|                 start: TIME_VALUE + OFFSET_START, | ||||
|                 end: TIME_VALUE + OFFSET_END | ||||
|             }, true); | ||||
|         }); | ||||
|  | ||||
|         it('will set up subscriptions correctly', () => { | ||||
|             expect(remoteClock._unsubscribe).toBeDefined(); | ||||
|             expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -1,9 +1,9 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * Open MCT Web, 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 | ||||
|  * Open MCT Web 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.
 | ||||
| @@ -14,37 +14,19 @@ | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * Open MCT Web 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> | ||||
| <li class="c-inspect-properties__row"> | ||||
|     <div class="c-inspect-properties__label"> | ||||
|         {{ detail.name }} | ||||
|     </div> | ||||
|     <div class="c-inspect-properties__value"> | ||||
|         {{ formattedTime }} | ||||
|     </div> | ||||
| </li> | ||||
| </template> | ||||
| import RemoteClock from "./RemoteClock"; | ||||
| /** | ||||
|  * Install a clock that uses a configurable telemetry endpoint. | ||||
|  */ | ||||
| 
 | ||||
| <script> | ||||
| import Moment from 'moment'; | ||||
| 
 | ||||
| export default { | ||||
|     props: { | ||||
|         detail: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         formattedTime() { | ||||
|             return Moment.utc(this.detail.value).format('YYYY-MM-DD[\n]HH:mm:ss') + ' UTC'; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| export default function (identifier) { | ||||
|     return function (openmct) { | ||||
|         openmct.time.addClock(new RemoteClock(openmct, identifier)); | ||||
|     }; | ||||
| } | ||||
| @@ -35,8 +35,8 @@ define([ | ||||
|  | ||||
|             this.removeColumnsForObject = this.removeColumnsForObject.bind(this); | ||||
|             this.objectMutated = this.objectMutated.bind(this); | ||||
|             //Make copy of configuration, otherwise change detection is impossible if shared instance is being modified. | ||||
|             this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration())); | ||||
|  | ||||
|             this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.objectMutated); | ||||
|         } | ||||
|  | ||||
|         getConfiguration() { | ||||
| @@ -58,14 +58,9 @@ define([ | ||||
|          * @private | ||||
|          * @param {*} object | ||||
|          */ | ||||
|         objectMutated(object) { | ||||
|             //Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible. | ||||
|             this.domainObject = object; | ||||
|             //Was it the configuration that changed? | ||||
|             if (object.configuration !== undefined && !_.eq(object.configuration, this.oldConfiguration)) { | ||||
|                 //Make copy of configuration, otherwise change detection is impossible if shared instance is being modified. | ||||
|                 this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration())); | ||||
|                 this.emit('change', object.configuration); | ||||
|         objectMutated(configuration) { | ||||
|             if (configuration !== undefined) { | ||||
|                 this.emit('change', configuration); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -162,7 +157,9 @@ define([ | ||||
|             this.updateConfiguration(configuration); | ||||
|         } | ||||
|  | ||||
|         destroy() {} | ||||
|         destroy() { | ||||
|             this.unlistenFromMutation(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return TelemetryTableConfiguration; | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/plugins/telemetryTable/TelemetryTableView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/plugins/telemetryTable/TelemetryTableView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import TableComponent from './components/table.vue'; | ||||
| import TelemetryTable from './TelemetryTable'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class TelemetryTableView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|  | ||||
|         this.table = new TelemetryTable(domainObject, openmct); | ||||
|     } | ||||
|  | ||||
|     getViewContext() { | ||||
|         if (!this.component) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         return this.component.$refs.tableComponent.getViewContext(); | ||||
|     } | ||||
|  | ||||
|     onEditModeChange(editMode) { | ||||
|         this.component.isEditing = editMode; | ||||
|     } | ||||
|  | ||||
|     onClearData() { | ||||
|         this.table.clearData(); | ||||
|     } | ||||
|  | ||||
|     getTable() { | ||||
|         return this.table; | ||||
|     } | ||||
|  | ||||
|     destroy(element) { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(element, editMode) { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
|                 TableComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 objectPath: this.objectPath, | ||||
|                 table: this.table, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             data() { | ||||
|                 return { | ||||
|                     isEditing: editMode, | ||||
|                     marking: { | ||||
|                         disableMultiSelect: false, | ||||
|                         enable: true, | ||||
|                         rowName: '', | ||||
|                         rowNamePlural: '', | ||||
|                         useAlternateControlBar: false | ||||
|                     } | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<table-component ref="tableComponent" :is-editing="isEditing" :marking="marking"></table-component>' | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -20,99 +20,35 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './components/table.vue', | ||||
|     './TelemetryTable', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     TableComponent, | ||||
|     TelemetryTable, | ||||
|     Vue | ||||
| ) { | ||||
|     function TelemetryTableViewProvider(openmct) { | ||||
|         function hasTelemetry(domainObject) { | ||||
|             if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|                 return false; | ||||
|             } | ||||
| import TelemetryTableView from './TelemetryTableView'; | ||||
|  | ||||
|             let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|             return metadata.values().length > 0; | ||||
| export default function TelemetryTableViewProvider(openmct) { | ||||
|     function hasTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             key: 'table', | ||||
|             name: 'Telemetry Table', | ||||
|             cssClass: 'icon-tabular-realtime', | ||||
|             canView(domainObject) { | ||||
|                 return domainObject.type === 'table' | ||||
|                     || hasTelemetry(domainObject); | ||||
|             }, | ||||
|             canEdit(domainObject) { | ||||
|                 return domainObject.type === 'table'; | ||||
|             }, | ||||
|             view(domainObject, objectPath) { | ||||
|                 let table = new TelemetryTable(domainObject, openmct); | ||||
|                 let component; | ||||
|                 let markingProp = { | ||||
|                     enable: true, | ||||
|                     useAlternateControlBar: false, | ||||
|                     rowName: '', | ||||
|                     rowNamePlural: '' | ||||
|                 }; | ||||
|                 const view = { | ||||
|                     show: function (element, editMode) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 TableComponent: TableComponent.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 table, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             data() { | ||||
|                                 return { | ||||
|                                     isEditing: editMode, | ||||
|                                     markingProp, | ||||
|                                     view | ||||
|                                 }; | ||||
|                             }, | ||||
|                             template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     onEditModeChange(editMode) { | ||||
|                         component.isEditing = editMode; | ||||
|                     }, | ||||
|                     onClearData() { | ||||
|                         table.clearData(); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.tableComponent.getViewContext(); | ||||
|                         } else { | ||||
|                             return { | ||||
|                                 type: 'telemetry-table' | ||||
|                             }; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function (element) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     }, | ||||
|                     _getTable: function () { | ||||
|                         return table; | ||||
|                     } | ||||
|                 }; | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|                 return view; | ||||
|             }, | ||||
|             priority() { | ||||
|                 return 1; | ||||
|             } | ||||
|         }; | ||||
|         return metadata.values().length > 0; | ||||
|     } | ||||
|  | ||||
|     return TelemetryTableViewProvider; | ||||
| }); | ||||
|     return { | ||||
|         key: 'table', | ||||
|         name: 'Telemetry Table', | ||||
|         cssClass: 'icon-tabular-realtime', | ||||
|         canView(domainObject) { | ||||
|             return domainObject.type === 'table' | ||||
|                 || hasTelemetry(domainObject); | ||||
|         }, | ||||
|         canEdit(domainObject) { | ||||
|             return domainObject.type === 'table'; | ||||
|         }, | ||||
|         view(domainObject, objectPath) { | ||||
|             return new TelemetryTableView(openmct, domainObject, objectPath); | ||||
|         }, | ||||
|         priority() { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -20,83 +20,89 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| let exportCSV = { | ||||
| const exportCSV = { | ||||
|     name: 'Export Table Data', | ||||
|     key: 'export-csv-all', | ||||
|     description: "Export this view's data", | ||||
|     cssClass: 'icon-download labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().exportAllDataAsCSV(); | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().exportAllDataAsCSV(); | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| let exportMarkedDataAsCSV = { | ||||
|  | ||||
| const exportMarkedDataAsCSV = { | ||||
|     name: 'Export Marked Rows', | ||||
|     key: 'export-csv-marked', | ||||
|     description: "Export marked rows as CSV", | ||||
|     cssClass: 'icon-download labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().exportMarkedDataAsCSV(); | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().exportMarkedDataAsCSV(); | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| let unmarkAllRows = { | ||||
|  | ||||
| const unmarkAllRows = { | ||||
|     name: 'Unmark All Rows', | ||||
|     key: 'unmark-all-rows', | ||||
|     description: 'Unmark all rows', | ||||
|     cssClass: 'icon-x labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().unmarkAllRows(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let pause = { | ||||
|     name: 'Pause', | ||||
|     key: 'pause-data', | ||||
|     description: 'Pause real-time data flow', | ||||
|     cssClass: 'icon-pause', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let play = { | ||||
|     name: 'Play', | ||||
|     key: 'play-data', | ||||
|     description: 'Continue real-time data flow', | ||||
|     cssClass: 'c-button pause-play is-paused', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let expandColumns = { | ||||
|     name: 'Expand Columns', | ||||
|     key: 'expand-columns', | ||||
|     description: "Increase column widths to fit currently available data.", | ||||
|     cssClass: 'icon-arrows-right-left labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().expandColumns(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let autosizeColumns = { | ||||
|     name: 'Autosize Columns', | ||||
|     key: 'autosize-columns', | ||||
|     description: "Automatically size columns to fit the table into the available space.", | ||||
|     cssClass: 'icon-expand labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().autosizeColumns(); | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().unmarkAllRows(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| let viewActions = [ | ||||
| const pause = { | ||||
|     name: 'Pause', | ||||
|     key: 'pause-data', | ||||
|     description: 'Pause real-time data flow', | ||||
|     cssClass: 'icon-pause', | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| const play = { | ||||
|     name: 'Play', | ||||
|     key: 'play-data', | ||||
|     description: 'Continue real-time data flow', | ||||
|     cssClass: 'c-button pause-play is-paused', | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| const expandColumns = { | ||||
|     name: 'Expand Columns', | ||||
|     key: 'expand-columns', | ||||
|     description: "Increase column widths to fit currently available data.", | ||||
|     cssClass: 'icon-arrows-right-left labeled', | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().expandColumns(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| const autosizeColumns = { | ||||
|     name: 'Autosize Columns', | ||||
|     key: 'autosize-columns', | ||||
|     description: "Automatically size columns to fit the table into the available space.", | ||||
|     cssClass: 'icon-expand labeled', | ||||
|     invoke: (objectPath, view) => { | ||||
|         view.getViewContext().autosizeColumns(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| const viewActions = [ | ||||
|     exportCSV, | ||||
|     exportMarkedDataAsCSV, | ||||
|     unmarkAllRows, | ||||
| @@ -107,16 +113,13 @@ let viewActions = [ | ||||
| ]; | ||||
|  | ||||
| viewActions.forEach(action => { | ||||
|     action.appliesTo = (objectPath, viewProvider = {}) => { | ||||
|         let viewContext = viewProvider.getViewContext && viewProvider.getViewContext(); | ||||
|  | ||||
|         if (viewContext) { | ||||
|             let type = viewContext.type; | ||||
|  | ||||
|             return type === 'telemetry-table'; | ||||
|     action.appliesTo = (objectPath, view = {}) => { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         if (!viewContext) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|         return viewContext.type === 'telemetry-table'; | ||||
|     }; | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export default { | ||||
|     components: { | ||||
|         TableCell | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'currentView'], | ||||
|     props: { | ||||
|         headers: { | ||||
|             type: Object, | ||||
| @@ -93,25 +93,11 @@ export default { | ||||
|             rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px', | ||||
|             rowClass: this.row.getRowClass(), | ||||
|             cellLimitClasses: this.row.getCellLimitClasses(), | ||||
|             componentList: Object.keys(this.headers).reduce((components, header) => { | ||||
|                 components[header] = this.row.getCellComponentName(header) || 'table-cell'; | ||||
|  | ||||
|                 return components; | ||||
|             }, {}), | ||||
|             selectableColumns: Object.keys(this.row.columns).reduce((selectable, columnKeys) => { | ||||
|                 selectable[columnKeys] = this.row.columns[columnKeys].selectable; | ||||
|  | ||||
|                 return selectable; | ||||
|             }, {}), | ||||
|             actionsViewContext: { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: this.getDatum | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
|             }, {}) | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -125,6 +111,13 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return listenersObject; | ||||
|         }, | ||||
|         componentList() { | ||||
|             return Object.keys(this.headers).reduce((components, header) => { | ||||
|                 components[header] = this.row.getCellComponentName(header) || 'table-cell'; | ||||
|  | ||||
|                 return components; | ||||
|             }, {}); | ||||
|         } | ||||
|     }, | ||||
|     // TODO: use computed properties | ||||
| @@ -185,19 +178,20 @@ export default { | ||||
|         showContextMenu: function (event) { | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             this.updateViewContext(); | ||||
|             this.markRow(event); | ||||
|  | ||||
|             this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => { | ||||
|                 let contextualObjectPath = this.objectPath.slice(); | ||||
|                 contextualObjectPath.unshift(domainObject); | ||||
|  | ||||
|                 let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext); | ||||
|                 let allActions = actionsCollection.getActionsObject(); | ||||
|                 let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]); | ||||
|  | ||||
|                 if (applicableActions.length) { | ||||
|                     this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|                 } | ||||
|             const actions = this.row.getContextMenuActions().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); | ||||
|             } | ||||
|         }, | ||||
|         updateViewContext() { | ||||
|             this.$emit('rowContextClick', { | ||||
|                 viewHistoricalData: true, | ||||
|                 viewDatumAction: true, | ||||
|                 getDatum: this.getDatum | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -233,6 +233,7 @@ | ||||
|                         @mark="markRow" | ||||
|                         @unmark="unmarkRow" | ||||
|                         @markMultipleConcurrent="markMultipleConcurrentRows" | ||||
|                         @rowContextClick="updateViewContext" | ||||
|                     /> | ||||
|                 </tbody> | ||||
|             </table> | ||||
| @@ -263,6 +264,7 @@ | ||||
|                 :column-widths="configuredColumnWidths" | ||||
|                 :row="sizingRowData" | ||||
|                 :object-path="objectPath" | ||||
|                 @rowContextClick="updateViewContext" | ||||
|             /> | ||||
|         </table> | ||||
|         <table-footer-indicator | ||||
| @@ -298,12 +300,25 @@ export default { | ||||
|         ToggleSwitch, | ||||
|         SizingRow | ||||
|     }, | ||||
|     inject: ['table', 'openmct', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'table', 'currentView'], | ||||
|     props: { | ||||
|         isEditing: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         marking: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return { | ||||
|                     enable: false, | ||||
|                     disableMultiSelect: false, | ||||
|                     useAlternateControlBar: false, | ||||
|                     rowName: '', | ||||
|                     rowNamePlural: '' | ||||
|                 }; | ||||
|             } | ||||
|         }, | ||||
|         allowExport: { | ||||
|             type: Boolean, | ||||
|             default: true | ||||
| @@ -316,28 +331,9 @@ export default { | ||||
|             type: Boolean, | ||||
|             default: true | ||||
|         }, | ||||
|         marking: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return { | ||||
|                     enable: false, | ||||
|                     disableMultiSelect: false, | ||||
|                     useAlternateControlBar: false, | ||||
|                     rowName: '', | ||||
|                     rowNamePlural: "" | ||||
|                 }; | ||||
|             } | ||||
|         }, | ||||
|         enableLegacyToolbar: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         view: { | ||||
|             type: Object, | ||||
|             required: false, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -373,7 +369,8 @@ export default { | ||||
|             isShowingMarkedRowsOnly: false, | ||||
|             enableRegexSearch: {}, | ||||
|             hideHeaders: configuration.hideHeaders, | ||||
|             totalNumberOfRows: 0 | ||||
|             totalNumberOfRows: 0, | ||||
|             rowContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -461,8 +458,10 @@ export default { | ||||
|         this.scroll = _.throttle(this.scroll, 100); | ||||
|  | ||||
|         if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) { | ||||
|             this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view); | ||||
|             this.initializeViewActions(); | ||||
|             this.$nextTick(() => { | ||||
|                 this.viewActionsCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); | ||||
|                 this.initializeViewActions(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.table.on('object-added', this.addObject); | ||||
| @@ -996,7 +995,8 @@ export default { | ||||
|                 unmarkAllRows: this.unmarkAllRows, | ||||
|                 togglePauseByButton: this.togglePauseByButton, | ||||
|                 expandColumns: this.recalculateColumnWidths, | ||||
|                 autosizeColumns: this.autosizeColumns | ||||
|                 autosizeColumns: this.autosizeColumns, | ||||
|                 row: this.rowContext | ||||
|             }; | ||||
|         }, | ||||
|         initializeViewActions() { | ||||
| @@ -1027,6 +1027,9 @@ export default { | ||||
|             this.setHeight(); | ||||
|             this.calculateTableSize(); | ||||
|             this.clearRowsAndRerender(); | ||||
|         }, | ||||
|         updateViewContext(rowContext) { | ||||
|             this.rowContext = rowContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -19,34 +19,26 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import TelemetryTableViewProvider from './TelemetryTableViewProvider'; | ||||
| import TableConfigurationViewProvider from './TableConfigurationViewProvider'; | ||||
| import TelemetryTableType from './TelemetryTableType'; | ||||
| import TelemetryTableViewActions from './ViewActions'; | ||||
|  | ||||
| define([ | ||||
|     './TelemetryTableViewProvider', | ||||
|     './TableConfigurationViewProvider', | ||||
|     './TelemetryTableType', | ||||
|     './ViewActions' | ||||
| ], function ( | ||||
|     TelemetryTableViewProvider, | ||||
|     TableConfigurationViewProvider, | ||||
|     TelemetryTableType, | ||||
|     TelemetryTableViewActions | ||||
| ) { | ||||
|     return function plugin() { | ||||
|         return function install(openmct) { | ||||
|             openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); | ||||
|             openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); | ||||
|             openmct.types.addType('table', TelemetryTableType); | ||||
|             openmct.composition.addPolicy((parent, child) => { | ||||
|                 if (parent.type === 'table') { | ||||
|                     return Object.prototype.hasOwnProperty.call(child, 'telemetry'); | ||||
|                 } else { | ||||
|                     return true; | ||||
|                 } | ||||
|             }); | ||||
| export default function plugin() { | ||||
|     return function install(openmct) { | ||||
|         openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); | ||||
|         openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); | ||||
|         openmct.types.addType('table', TelemetryTableType); | ||||
|         openmct.composition.addPolicy((parent, child) => { | ||||
|             if (parent.type === 'table') { | ||||
|                 return Object.prototype.hasOwnProperty.call(child, 'telemetry'); | ||||
|             } else { | ||||
|                 return true; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|             TelemetryTableViewActions.default.forEach(action => { | ||||
|                 openmct.actions.register(action); | ||||
|             }); | ||||
|         }; | ||||
|         TelemetryTableViewActions.forEach(action => { | ||||
|             openmct.actions.register(action); | ||||
|         }); | ||||
|     }; | ||||
| }); | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,7 @@ describe("the plugin", () => { | ||||
|     let tablePlugin; | ||||
|     let element; | ||||
|     let child; | ||||
|     let unlistenConfigMutation; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
| @@ -87,6 +88,10 @@ describe("the plugin", () => { | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         if (unlistenConfigMutation) { | ||||
|             unlistenConfigMutation(); | ||||
|         } | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
| @@ -121,6 +126,11 @@ describe("the plugin", () => { | ||||
|         let tableInstance; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             }); | ||||
|  | ||||
|             testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
| @@ -149,6 +159,14 @@ describe("the plugin", () => { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 }, | ||||
|                 configuration: { | ||||
|                     hiddenColumns: { | ||||
|                         name: false, | ||||
|                         utc: false, | ||||
|                         'some-key': false, | ||||
|                         'some-other-key': false | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             const testTelemetry = [ | ||||
| @@ -185,7 +203,7 @@ describe("the plugin", () => { | ||||
|             tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); | ||||
|             tableView.show(child, true); | ||||
|  | ||||
|             tableInstance = tableView._getTable(); | ||||
|             tableInstance = tableView.getTable(); | ||||
|  | ||||
|             return telemetryPromise.then(() => Vue.nextTick()); | ||||
|         }); | ||||
| @@ -272,5 +290,39 @@ describe("the plugin", () => { | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("displays the correct number of column headers when the configuration is mutated", async () => { | ||||
|             const tableInstanceConfiguration = tableInstance.domainObject.configuration; | ||||
|             tableInstanceConfiguration.hiddenColumns['some-key'] = true; | ||||
|             unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); | ||||
|             expect(tableHeaderElements.length).toEqual(3); | ||||
|  | ||||
|             tableInstanceConfiguration.hiddenColumns['some-key'] = false; | ||||
|             unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); | ||||
|             expect(tableHeaderElements.length).toEqual(4); | ||||
|         }); | ||||
|  | ||||
|         it("displays the correct number of table cells in a row when the configuration is mutated", async () => { | ||||
|             const tableInstanceConfiguration = tableInstance.domainObject.configuration; | ||||
|             tableInstanceConfiguration.hiddenColumns['some-key'] = true; | ||||
|             unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             let tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td'); | ||||
|             expect(tableRowCells.length).toEqual(3); | ||||
|  | ||||
|             tableInstanceConfiguration.hiddenColumns['some-key'] = false; | ||||
|             unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td'); | ||||
|             expect(tableRowCells.length).toEqual(4); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -85,7 +85,7 @@ | ||||
|                 <button | ||||
|                     ref="startOffset" | ||||
|                     class="c-button c-conductor__delta-button" | ||||
|                     @click="showTimePopupStart" | ||||
|                     @click.prevent="showTimePopupStart" | ||||
|                 > | ||||
|                     {{ offsets.start }} | ||||
|                 </button> | ||||
| @@ -133,7 +133,7 @@ | ||||
|                 <button | ||||
|                     ref="endOffset" | ||||
|                     class="c-button c-conductor__delta-button" | ||||
|                     @click="showTimePopupEnd" | ||||
|                     @click.prevent="showTimePopupEnd" | ||||
|                 > | ||||
|                     {{ offsets.end }} | ||||
|                 </button> | ||||
|   | ||||
| @@ -151,7 +151,7 @@ export default { | ||||
|                     cssClass: 'icon-history', | ||||
|                     name, | ||||
|                     description, | ||||
|                     callBack: () => this.selectTimespan(timespan) | ||||
|                     onItemClicked: () => this.selectTimespan(timespan) | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
| @@ -160,7 +160,7 @@ export default { | ||||
|                 description: 'Past timeframes, ordered by latest first', | ||||
|                 isDisabled: true, | ||||
|                 name: 'Past timeframes, ordered by latest first', | ||||
|                 callBack: () => {} | ||||
|                 onItemClicked: () => {} | ||||
|             }); | ||||
|  | ||||
|             return history; | ||||
| @@ -171,7 +171,7 @@ export default { | ||||
|                     cssClass: 'icon-clock', | ||||
|                     name: preset.label, | ||||
|                     description: preset.label, | ||||
|                     callBack: () => this.selectPresetBounds(preset.bounds) | ||||
|                     onItemClicked: () => this.selectPresetBounds(preset.bounds) | ||||
|                 }; | ||||
|             }); | ||||
|         }, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user