Compare commits
	
		
			75 Commits
		
	
	
		
			plot-perfo
			...
			1.7.8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ab7e2c5747 | ||
|   | 4d0487631b | ||
|   | 8b53618adb | ||
|   | 364a97d8b7 | ||
|   | de5da9445b | ||
|   | 53d63d69fe | ||
|   | c5350659e2 | ||
|   | 76830b24ac | ||
|   | 99e90c7488 | ||
|   | 2c4fb4cb9d | ||
|   | c91b9f7825 | ||
|   | 166211a8be | ||
|   | c4e41d784a | ||
|   | 35eceea793 | ||
|   | 903a44fd80 | ||
|   | 3c546a0a1f | ||
|   | b6786b2be3 | ||
|   | 4e13b3ff43 | ||
|   | c20369d9bf | ||
|   | f58cd4b9ce | ||
|   | 476128ced8 | ||
|   | 22c812d67d | ||
|   | 648f3532c1 | ||
|   | 4fe44a5619 | ||
|   | d221610df9 | ||
|   | 8243cf5d7b | ||
|   | c4c1fea17f | ||
|   | 5e920e90ce | ||
|   | 886db23eb6 | ||
|   | 0ccb546a2e | ||
|   | 271f8ed38f | ||
|   | 650f84e95c | ||
|   | b70af5a1bb | ||
|   | 0af21632db | ||
|   | e2f1ff5442 | ||
|   | c4b9be18f1 | ||
|   | eabdf6cd04 | ||
|   | e56c673005 | ||
|   | dad9f12a5c | ||
|   | aa5edb0b83 | ||
|   | b315803180 | ||
|   | b27317631b | ||
|   | 953a9daafb | ||
|   | 63f9cd449f | ||
|   | 54220f547b | ||
|   | 93d967c2b3 | ||
|   | 1226459c6f | ||
|   | d7c9c9cb98 | ||
|   | 2131ef2397 | ||
|   | 48c22369a1 | ||
|   | 6506077f4d | ||
|   | b1b4266ff3 | ||
|   | 42b0148f93 | ||
|   | 9461ad8edd | ||
|   | 40055ba955 | ||
|   | 9cb85ad176 | ||
|   | f2b2953a5d | ||
|   | 62de310686 | ||
|   | 4b9ff67e49 | ||
|   | d5e32ec494 | ||
|   | 38880ba3d1 | ||
|   | a99ce7733c | ||
|   | 9f48764210 | ||
|   | a1aaa0dd41 | ||
|   | bee15e98c8 | ||
|   | 092bbe547d | ||
|   | 6cbe05317c | ||
|   | 3b92fcdf6c | ||
|   | 6dde54bd25 | ||
|   | 359e7377ac | ||
|   | 9f4190f781 | ||
|   | f3fc991a74 | ||
|   | 2564e75fc9 | ||
|   | f42fe78acf | ||
|   | fe928a1386 | 
| @@ -56,14 +56,38 @@ workflows: | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: false | ||||
|       - test: | ||||
|           name: node12-firefoxESR | ||||
|           name: node12-firefoxESR-build-only | ||||
|           node-version: lts/erbium | ||||
|           browser: FirefoxESR | ||||
|           always-pass: true | ||||
|       - test: | ||||
|           name: node14-chrome | ||||
|           name: node14-chrome-build-only | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: true | ||||
|   nightly: | ||||
|     jobs: | ||||
|       - test: | ||||
|           name: node10-chrome-nightly | ||||
|           node-version: lts/dubnium | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: false | ||||
|       - test: | ||||
|           name: node12-firefoxESR-nightly | ||||
|           node-version: lts/erbium | ||||
|           browser: FirefoxESR | ||||
|           always-pass: false | ||||
|       - test: | ||||
|           name: node14-chrome-nightly | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: false | ||||
|     triggers: | ||||
|       - schedule: | ||||
|           cron: "0 0 * * *" | ||||
|           filters: | ||||
|             branches: | ||||
|               only: | ||||
|                 - master       | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
|  | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|   schedule: | ||||
|     - cron: '28 21 * * 3' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: javascript | ||||
|  | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
							
								
								
									
										2
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								API.md
									
									
									
									
									
								
							| @@ -996,7 +996,7 @@ reveal additional information when the mouse cursor is hovered over it. | ||||
| A common use case for indicators is to convey the state of some external system such as a  | ||||
| persistence backend or HTTP server. So long as this system is accessible via HTTP request,  | ||||
| Open MCT provides a general purpose indicator to show whether the server is available and  | ||||
| returing a 2xx status code. The URL Status Indicator is made available as a default plugin. See | ||||
| returning a 2xx status code. The URL Status Indicator is made available as a default plugin. See | ||||
| the [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the  | ||||
| URL Status Indicator. | ||||
|  | ||||
|   | ||||
| @@ -423,7 +423,7 @@ which can help with this, however. | ||||
|   instead of separate approaches for static and substitutable | ||||
|   dependencies. | ||||
| * Removes need to understand Angular's DI mechanism. | ||||
| * Improves useability of documentation (`typeService` is an | ||||
| * Improves usability of documentation (`typeService` is an | ||||
|   instance of `CompositeService` and implements `TypeService` | ||||
|   so you can easily traverse links in the JSDoc.) | ||||
| * Can be used more easily from Web Workers, allowing services | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
| ## Legacy Documentation | ||||
|  | ||||
| As we transition to a new API, the following documentation for the old API | ||||
| (which is supported during the transtion) may be useful as well: | ||||
| (which is supported during the transition) may be useful as well: | ||||
|  | ||||
|  * The [Architecture Overview](architecture/) describes the concepts used | ||||
|  throughout Open MCT, and gives a high level overview of the platform's design. | ||||
|   | ||||
| @@ -28,6 +28,15 @@ define([ | ||||
|                         domain: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         domain: 3 | ||||
|                     } | ||||
|                 }, | ||||
|                 // Need to enable "LocalTimeSystem" plugin to make use of this | ||||
|                 // { | ||||
|                 //     key: "local", | ||||
|   | ||||
| @@ -63,7 +63,7 @@ define([ | ||||
|  | ||||
|     StateGeneratorProvider.prototype.request = function (domainObject, options) { | ||||
|         var start = options.start; | ||||
|         var end = options.end; | ||||
|         var end = Math.min(Date.now(), options.end); // no future values | ||||
|         var duration = domainObject.telemetry.duration * 1000; | ||||
|         if (options.strategy === 'latest' || options.size === 1) { | ||||
|             start = end; | ||||
|   | ||||
| @@ -54,23 +54,38 @@ | ||||
|         var start = Date.now(); | ||||
|         var step = 1000 / data.dataRateInHz; | ||||
|         var nextStep = start - (start % step) + step; | ||||
|         let work; | ||||
|         if (data.spectra) { | ||||
|             work = function (now) { | ||||
|                 while (nextStep < now) { | ||||
|                     const messageCopy = Object.create(message); | ||||
|                     message.data.start = nextStep - (60 * 1000); | ||||
|                     message.data.end = nextStep; | ||||
|                     onRequest(messageCopy); | ||||
|                     nextStep += step; | ||||
|                 } | ||||
|  | ||||
|         function work(now) { | ||||
|             while (nextStep < now) { | ||||
|                 self.postMessage({ | ||||
|                     id: message.id, | ||||
|                     data: { | ||||
|                         name: data.name, | ||||
|                         utc: nextStep, | ||||
|                         yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                         sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                         cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                     } | ||||
|                 }); | ||||
|                 nextStep += step; | ||||
|             } | ||||
|                 return nextStep; | ||||
|             }; | ||||
|         } else { | ||||
|             work = function (now) { | ||||
|                 while (nextStep < now) { | ||||
|                     self.postMessage({ | ||||
|                         id: message.id, | ||||
|                         data: { | ||||
|                             name: data.name, | ||||
|                             utc: nextStep, | ||||
|                             yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                             sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                             wavelength: wavelength(start, nextStep), | ||||
|                             cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                         } | ||||
|                     }); | ||||
|                     nextStep += step; | ||||
|                 } | ||||
|  | ||||
|             return nextStep; | ||||
|                 return nextStep; | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         subscriptions[message.id] = work; | ||||
| @@ -111,13 +126,21 @@ | ||||
|                 utc: nextStep, | ||||
|                 yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                 sin: sin(nextStep, period, amplitude, offset, phase, randomness), | ||||
|                 wavelength: wavelength(start, nextStep), | ||||
|                 cos: cos(nextStep, period, amplitude, offset, phase, randomness) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         self.postMessage({ | ||||
|             id: message.id, | ||||
|             data: data | ||||
|             data: request.spectra ? { | ||||
|                 wavelength: data.map((item) => { | ||||
|                     return item.wavelength; | ||||
|                 }), | ||||
|                 cos: data.map((item) => { | ||||
|                     return item.cos; | ||||
|                 }) | ||||
|             } : data | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -131,6 +154,10 @@ | ||||
|             * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; | ||||
|     } | ||||
|  | ||||
|     function wavelength(start, nextStep) { | ||||
|         return (nextStep - start) / 10; | ||||
|     } | ||||
|  | ||||
|     function sendError(error, message) { | ||||
|         self.postMessage({ | ||||
|             error: error.name + ': ' + error.message, | ||||
|   | ||||
| @@ -152,7 +152,7 @@ | ||||
|         <h2>How to Use Glyphs</h2> | ||||
|         <div class="cols cols1-1"> | ||||
|             <div class="col"> | ||||
|                 <p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a psuedo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p> | ||||
|                 <p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a pseudo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p> | ||||
|                 <p>Alternately, you can use the <code>.ui-symbol</code> class in an object that contains encoded HTML entities. This method is only recommended if you cannot use the aforementioned CSS class approach.</p> | ||||
|             </div> | ||||
|             <mct-example><a class="s-button icon-gear" title="Settings"></a> | ||||
|   | ||||
| @@ -195,6 +195,7 @@ | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], | ||||
|             {indicator: true} | ||||
|         )); | ||||
|         openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); | ||||
|         openmct.start(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,9 +1,9 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.6-SNAPSHOT", | ||||
|   "version": "1.7.8", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|     "@braintree/sanitize-url": "^5.0.2", | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
|     "babel-eslint": "10.0.3", | ||||
| @@ -12,16 +12,9 @@ | ||||
|     "copy-webpack-plugin": "^4.5.2", | ||||
|     "cross-env": "^6.0.3", | ||||
|     "css-loader": "^1.0.0", | ||||
|     "d3-array": "1.2.x", | ||||
|     "d3-axis": "1.0.x", | ||||
|     "d3-collection": "1.0.x", | ||||
|     "d3-color": "1.0.x", | ||||
|     "d3-format": "1.2.x", | ||||
|     "d3-interpolate": "1.1.x", | ||||
|     "d3-scale": "1.0.x", | ||||
|     "d3-selection": "1.3.x", | ||||
|     "d3-time": "1.0.x", | ||||
|     "d3-time-format": "2.1.x", | ||||
|     "eslint": "7.0.0", | ||||
|     "eslint-plugin-vue": "^7.5.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", | ||||
| @@ -41,13 +34,13 @@ | ||||
|     "jsdoc": "^3.3.2", | ||||
|     "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-junit-reporter": "2.0.1", | ||||
|     "karma-firefox-launcher": "2.1.1", | ||||
|     "karma-html-reporter": "0.2.7", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-webpack": "4.0.2", | ||||
|     "location-bar": "^3.0.1", | ||||
| @@ -62,6 +55,8 @@ | ||||
|     "node-bourbon": "^4.2.3", | ||||
|     "node-sass": "^4.14.1", | ||||
|     "painterro": "^1.2.56", | ||||
|     "plotly.js-basic-dist": "^2.5.0", | ||||
|     "plotly.js-gl2d-dist": "^2.5.0", | ||||
|     "printj": "^1.2.1", | ||||
|     "raw-loader": "^0.5.1", | ||||
|     "request": "^2.69.0", | ||||
|   | ||||
| @@ -64,7 +64,7 @@ define( | ||||
|          * | ||||
|          * @param {DomainObject} domainObject the domain object to navigate to | ||||
|          * @param {Boolean} force if true, force navigation to occur. | ||||
|          * @returns {Boolean} true if navigation occured, otherwise false. | ||||
|          * @returns {Boolean} true if navigation occurred, otherwise false. | ||||
|          */ | ||||
|         NavigationService.prototype.setNavigation = function (domainObject, force) { | ||||
|             if (force) { | ||||
|   | ||||
| @@ -21,28 +21,14 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "./src/MCTDevice", | ||||
|     "./src/AgentService", | ||||
|     "./src/DeviceClassifier" | ||||
|     "./src/AgentService" | ||||
| ], function ( | ||||
|     MCTDevice, | ||||
|     AgentService, | ||||
|     DeviceClassifier | ||||
|     AgentService | ||||
| ) { | ||||
|  | ||||
|     return { | ||||
|         name: "platform/commonUI/mobile", | ||||
|         definition: { | ||||
|             "extensions": { | ||||
|                 "directives": [ | ||||
|                     { | ||||
|                         "key": "mctDevice", | ||||
|                         "implementation": MCTDevice, | ||||
|                         "depends": [ | ||||
|                             "agentService" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 "services": [ | ||||
|                     { | ||||
|                         "key": "agentService", | ||||
| @@ -51,15 +37,6 @@ define([ | ||||
|                             "$window" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 "runs": [ | ||||
|                     { | ||||
|                         "implementation": DeviceClassifier, | ||||
|                         "depends": [ | ||||
|                             "agentService", | ||||
|                             "$document" | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -20,122 +20,12 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /** | ||||
|  * Provides features which support variant behavior on mobile devices. | ||||
|  * | ||||
|  * @namespace platform/commonUI/mobile | ||||
|  */ | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
| define(["../../../../src/utils/agent/Agent.js"], function (Agent) { | ||||
|     function AngularAgentServiceWrapper(window) { | ||||
|         const AS = Agent.default; | ||||
|  | ||||
|         /** | ||||
|          * The query service handles calls for browser and userAgent | ||||
|          * info using a comparison between the userAgent and key | ||||
|          * device names | ||||
|          * @constructor | ||||
|          * @param $window Angular-injected instance of the window | ||||
|          * @memberof platform/commonUI/mobile | ||||
|          */ | ||||
|         function AgentService($window) { | ||||
|             var userAgent = $window.navigator.userAgent, | ||||
|                 matches = userAgent.match(/iPad|iPhone|Android/i) || []; | ||||
|  | ||||
|             this.userAgent = userAgent; | ||||
|             this.mobileName = matches[0]; | ||||
|             this.$window = $window; | ||||
|             this.touchEnabled = ($window.ontouchstart !== undefined); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Check if the user is on a mobile device. | ||||
|          * @returns {boolean} true on mobile | ||||
|          */ | ||||
|         AgentService.prototype.isMobile = function () { | ||||
|             return Boolean(this.mobileName); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user is on a phone-sized mobile device. | ||||
|          * @returns {boolean} true on a phone | ||||
|          */ | ||||
|         AgentService.prototype.isPhone = function () { | ||||
|             if (this.isMobile()) { | ||||
|                 if (this.isAndroidTablet()) { | ||||
|                     return false; | ||||
|                 } else if (this.mobileName === 'iPad') { | ||||
|                     return false; | ||||
|                 } else { | ||||
|                     return true; | ||||
|                 } | ||||
|             } else { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user is on a tablet sized android device | ||||
|          * @returns {boolean} true on an android tablet | ||||
|          */ | ||||
|         AgentService.prototype.isAndroidTablet = function () { | ||||
|             if (this.mobileName === 'Android') { | ||||
|                 if (this.isPortrait() && window.innerWidth >= 768) { | ||||
|                     return true; | ||||
|                 } else if (this.isLandscape() && window.innerHeight >= 768) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } else { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user is on a tablet-sized mobile device. | ||||
|          * @returns {boolean} true on a tablet | ||||
|          */ | ||||
|         AgentService.prototype.isTablet = function () { | ||||
|             return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet()); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user's device is in a portrait-style | ||||
|          * orientation (display width is narrower than display height.) | ||||
|          * @returns {boolean} true in portrait mode | ||||
|          */ | ||||
|         AgentService.prototype.isPortrait = function () { | ||||
|             return this.$window.innerWidth < this.$window.innerHeight; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user's device is in a landscape-style | ||||
|          * orientation (display width is greater than display height.) | ||||
|          * @returns {boolean} true in landscape mode | ||||
|          */ | ||||
|         AgentService.prototype.isLandscape = function () { | ||||
|             return !this.isPortrait(); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user's device supports a touch interface. | ||||
|          * @returns {boolean} true if touch is supported | ||||
|          */ | ||||
|         AgentService.prototype.isTouch = function () { | ||||
|             return this.touchEnabled; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user agent matches a certain named device, | ||||
|          * as indicated by checking for a case-insensitive substring | ||||
|          * match. | ||||
|          * @param {string} name the name to check for | ||||
|          * @returns {boolean} true if the user agent includes that name | ||||
|          */ | ||||
|         AgentService.prototype.isBrowser = function (name) { | ||||
|             name = name.toLowerCase(); | ||||
|  | ||||
|             return this.userAgent.toLowerCase().indexOf(name) !== -1; | ||||
|         }; | ||||
|  | ||||
|         return AgentService; | ||||
|         return new AS(window); | ||||
|     } | ||||
| ); | ||||
|  | ||||
|     return AngularAgentServiceWrapper; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										96
									
								
								platform/commonUI/mobile/src/AgentServiceSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								platform/commonUI/mobile/src/AgentServiceSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 AgentService from "./AgentService"; | ||||
|  | ||||
| const TEST_USER_AGENTS = { | ||||
|     DESKTOP: | ||||
|     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", | ||||
|     IPAD: | ||||
|     "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", | ||||
|     IPHONE: | ||||
|     "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53" | ||||
| }; | ||||
|  | ||||
| describe("The AgentService", function () { | ||||
|     let testWindow; | ||||
|     let agentService; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         testWindow = { | ||||
|             innerWidth: 640, | ||||
|             innerHeight: 480, | ||||
|             navigator: { | ||||
|                 userAgent: TEST_USER_AGENTS.DESKTOP | ||||
|             } | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     it("recognizes desktop devices as non-mobile", function () { | ||||
|         testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; | ||||
|         agentService = new AgentService(testWindow); | ||||
|         expect(agentService.isMobile()).toBeFalsy(); | ||||
|         expect(agentService.isPhone()).toBeFalsy(); | ||||
|         expect(agentService.isTablet()).toBeFalsy(); | ||||
|     }); | ||||
|  | ||||
|     it("detects iPhones", function () { | ||||
|         testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; | ||||
|         agentService = new AgentService(testWindow); | ||||
|         expect(agentService.isMobile()).toBeTruthy(); | ||||
|         expect(agentService.isPhone()).toBeTruthy(); | ||||
|         expect(agentService.isTablet()).toBeFalsy(); | ||||
|     }); | ||||
|  | ||||
|     it("detects iPads", function () { | ||||
|         testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; | ||||
|         agentService = new AgentService(testWindow); | ||||
|         expect(agentService.isMobile()).toBeTruthy(); | ||||
|         expect(agentService.isPhone()).toBeFalsy(); | ||||
|         expect(agentService.isTablet()).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("detects display orientation", function () { | ||||
|         agentService = new AgentService(testWindow); | ||||
|         testWindow.innerWidth = 1024; | ||||
|         testWindow.innerHeight = 400; | ||||
|         expect(agentService.isPortrait()).toBeFalsy(); | ||||
|         expect(agentService.isLandscape()).toBeTruthy(); | ||||
|         testWindow.innerWidth = 400; | ||||
|         testWindow.innerHeight = 1024; | ||||
|         expect(agentService.isPortrait()).toBeTruthy(); | ||||
|         expect(agentService.isLandscape()).toBeFalsy(); | ||||
|     }); | ||||
|  | ||||
|     it("detects touch support", function () { | ||||
|         testWindow.ontouchstart = null; | ||||
|         expect(new AgentService(testWindow).isTouch()).toBe(true); | ||||
|         delete testWindow.ontouchstart; | ||||
|         expect(new AgentService(testWindow).isTouch()).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it("allows for checking browser type", function () { | ||||
|         testWindow.navigator.userAgent = "Chromezilla Safarifox"; | ||||
|         agentService = new AgentService(testWindow); | ||||
|         expect(agentService.isBrowser("Chrome")).toBe(true); | ||||
|         expect(agentService.isBrowser("Firefox")).toBe(false); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,72 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['./DeviceMatchers'], | ||||
|     function (DeviceMatchers) { | ||||
|  | ||||
|         /** | ||||
|          * Runs at application startup and adds a subset of the following | ||||
|          * CSS classes to the body of the document, depending on device | ||||
|          * attributes: | ||||
|          * | ||||
|          * * `mobile`: Phones or tablets. | ||||
|          * * `phone`: Phones specifically. | ||||
|          * * `tablet`: Tablets specifically. | ||||
|          * * `desktop`: Non-mobile devices. | ||||
|          * * `portrait`: Devices in a portrait-style orientation. | ||||
|          * * `landscape`: Devices in a landscape-style orientation. | ||||
|          * * `touch`: Device supports touch events. | ||||
|          * | ||||
|          * @param {platform/commonUI/mobile.AgentService} agentService | ||||
|          *        the service used to examine the user agent | ||||
|          * @param $document Angular's jqLite-wrapped document element | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MobileClassifier(agentService, $document) { | ||||
|             var body = $document.find('body'); | ||||
|  | ||||
|             Object.keys(DeviceMatchers).forEach(function (key, index, array) { | ||||
|                 if (DeviceMatchers[key](agentService)) { | ||||
|                     body.addClass(key); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (agentService.isMobile()) { | ||||
|                 var mediaQuery = window.matchMedia('(orientation: landscape)'); | ||||
|  | ||||
|                 mediaQuery.addListener(function (event) { | ||||
|                     if (event.matches) { | ||||
|                         body.removeClass('portrait'); | ||||
|                         body.addClass('landscape'); | ||||
|                     } else { | ||||
|                         body.removeClass('landscape'); | ||||
|                         body.addClass('portrait'); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return MobileClassifier; | ||||
|  | ||||
|     } | ||||
| ); | ||||
| @@ -1,88 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['./DeviceMatchers'], | ||||
|     function (DeviceMatchers) { | ||||
|  | ||||
|         /** | ||||
|          * The `mct-device` directive, when applied as an attribute, | ||||
|          * only includes the element when the device being used matches | ||||
|          * a set of characteristics required. | ||||
|          * | ||||
|          * Required characteristics are given as space-separated strings | ||||
|          * as the value to this attribute, e.g.: | ||||
|          * | ||||
|          *    <span mct-device="mobile portrait">Hello world!</span> | ||||
|          * | ||||
|          * ...will only show Hello world! when viewed on a mobile device | ||||
|          * in the portrait orientation. | ||||
|          * | ||||
|          * Valid device characteristics to detect are: | ||||
|          * | ||||
|          * * `mobile`: Phones or tablets. | ||||
|          * * `phone`: Phones specifically. | ||||
|          * * `tablet`: Tablets specifically. | ||||
|          * * `desktop`: Non-mobile devices. | ||||
|          * * `portrait`: Devices in a portrait-style orientation. | ||||
|          * * `landscape`: Devices in a landscape-style orientation. | ||||
|          * * `touch`: Device supports touch events. | ||||
|          * | ||||
|          * @param {AgentService} agentService used to detect device type | ||||
|          *        based on information about the user agent | ||||
|          */ | ||||
|         function MCTDevice(agentService) { | ||||
|  | ||||
|             function deviceMatches(tokens) { | ||||
|                 tokens = tokens || ""; | ||||
|  | ||||
|                 return tokens.split(" ").every(function (token) { | ||||
|                     var fn = DeviceMatchers[token]; | ||||
|  | ||||
|                     return fn && fn(agentService); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function link(scope, element, attrs, ctrl, transclude) { | ||||
|                 if (deviceMatches(attrs.mctDevice)) { | ||||
|                     transclude(function (clone) { | ||||
|                         element.replaceWith(clone); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 link: link, | ||||
|                 // We are transcluding the whole element (like ng-if) | ||||
|                 transclude: 'element', | ||||
|                 // 1 more than ng-if | ||||
|                 priority: 601, | ||||
|                 // Also terminal, since element will be transcluded | ||||
|                 terminal: true, | ||||
|                 // Only apply as an attribute | ||||
|                 restrict: "A" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return MCTDevice; | ||||
|     } | ||||
| ); | ||||
| @@ -1,99 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../src/AgentService"], | ||||
|     function (AgentService) { | ||||
|  | ||||
|         var TEST_USER_AGENTS = { | ||||
|             DESKTOP: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", | ||||
|             IPAD: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", | ||||
|             IPHONE: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53" | ||||
|         }; | ||||
|  | ||||
|         describe("The AgentService", function () { | ||||
|             var testWindow, agentService; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testWindow = { | ||||
|                     innerWidth: 640, | ||||
|                     innerHeight: 480, | ||||
|                     navigator: { | ||||
|                         userAgent: TEST_USER_AGENTS.DESKTOP | ||||
|                     } | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             it("recognizes desktop devices as non-mobile", function () { | ||||
|                 testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 expect(agentService.isMobile()).toBeFalsy(); | ||||
|                 expect(agentService.isPhone()).toBeFalsy(); | ||||
|                 expect(agentService.isTablet()).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("detects iPhones", function () { | ||||
|                 testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 expect(agentService.isMobile()).toBeTruthy(); | ||||
|                 expect(agentService.isPhone()).toBeTruthy(); | ||||
|                 expect(agentService.isTablet()).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("detects iPads", function () { | ||||
|                 testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 expect(agentService.isMobile()).toBeTruthy(); | ||||
|                 expect(agentService.isPhone()).toBeFalsy(); | ||||
|                 expect(agentService.isTablet()).toBeTruthy(); | ||||
|             }); | ||||
|  | ||||
|             it("detects display orientation", function () { | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 testWindow.innerWidth = 1024; | ||||
|                 testWindow.innerHeight = 400; | ||||
|                 expect(agentService.isPortrait()).toBeFalsy(); | ||||
|                 expect(agentService.isLandscape()).toBeTruthy(); | ||||
|                 testWindow.innerWidth = 400; | ||||
|                 testWindow.innerHeight = 1024; | ||||
|                 expect(agentService.isPortrait()).toBeTruthy(); | ||||
|                 expect(agentService.isLandscape()).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("detects touch support", function () { | ||||
|                 testWindow.ontouchstart = null; | ||||
|                 expect(new AgentService(testWindow).isTouch()) | ||||
|                     .toBe(true); | ||||
|                 delete testWindow.ontouchstart; | ||||
|                 expect(new AgentService(testWindow).isTouch()) | ||||
|                     .toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("allows for checking browser type", function () { | ||||
|                 testWindow.navigator.userAgent = "Chromezilla Safarifox"; | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 expect(agentService.isBrowser("Chrome")).toBe(true); | ||||
|                 expect(agentService.isBrowser("Firefox")).toBe(false); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,109 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../src/DeviceClassifier", "../src/DeviceMatchers"], | ||||
|     function (DeviceClassifier, DeviceMatchers) { | ||||
|  | ||||
|         var AGENT_SERVICE_METHODS = [ | ||||
|                 'isMobile', | ||||
|                 'isPhone', | ||||
|                 'isTablet', | ||||
|                 'isPortrait', | ||||
|                 'isLandscape', | ||||
|                 'isTouch' | ||||
|             ], | ||||
|             TEST_PERMUTATIONS = [ | ||||
|                 ['isMobile', 'isPhone', 'isTouch', 'isPortrait'], | ||||
|                 ['isMobile', 'isPhone', 'isTouch', 'isLandscape'], | ||||
|                 ['isMobile', 'isTablet', 'isTouch', 'isPortrait'], | ||||
|                 ['isMobile', 'isTablet', 'isTouch', 'isLandscape'], | ||||
|                 ['isTouch'], | ||||
|                 [] | ||||
|             ]; | ||||
|  | ||||
|         describe("DeviceClassifier", function () { | ||||
|             var mockAgentService, | ||||
|                 mockDocument, | ||||
|                 mockBody; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     'agentService', | ||||
|                     AGENT_SERVICE_METHODS | ||||
|                 ); | ||||
|                 mockDocument = jasmine.createSpyObj( | ||||
|                     '$document', | ||||
|                     ['find'] | ||||
|                 ); | ||||
|                 mockBody = jasmine.createSpyObj( | ||||
|                     'body', | ||||
|                     ['addClass'] | ||||
|                 ); | ||||
|                 mockDocument.find.and.callFake(function (sel) { | ||||
|                     return sel === 'body' && mockBody; | ||||
|                 }); | ||||
|                 AGENT_SERVICE_METHODS.forEach(function (m) { | ||||
|                     mockAgentService[m].and.returnValue(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             TEST_PERMUTATIONS.forEach(function (trueMethods) { | ||||
|                 var summary = trueMethods.length === 0 | ||||
|                     ? "device has no detected characteristics" | ||||
|                     : "device " + (trueMethods.join(", ")); | ||||
|  | ||||
|                 describe("when " + summary, function () { | ||||
|                     var classifier; // eslint-disable-line | ||||
|  | ||||
|                     beforeEach(function () { | ||||
|                         trueMethods.forEach(function (m) { | ||||
|                             mockAgentService[m].and.returnValue(true); | ||||
|                         }); | ||||
|                         classifier = new DeviceClassifier( | ||||
|                             mockAgentService, | ||||
|                             mockDocument | ||||
|                         ); | ||||
|                     }); | ||||
|  | ||||
|                     it("adds classes for matching, detected characteristics", function () { | ||||
|                         Object.keys(DeviceMatchers).filter(function (m) { | ||||
|                             return DeviceMatchers[m](mockAgentService); | ||||
|                         }).forEach(function (key) { | ||||
|                             expect(mockBody.addClass) | ||||
|                                 .toHaveBeenCalledWith(key); | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     it("does not add classes for non-matching characteristics", function () { | ||||
|                         Object.keys(DeviceMatchers).filter(function (m) { | ||||
|                             return !DeviceMatchers[m](mockAgentService); | ||||
|                         }).forEach(function (key) { | ||||
|                             expect(mockBody.addClass) | ||||
|                                 .not.toHaveBeenCalledWith(key); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,78 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../src/DeviceMatchers"], | ||||
|     function (DeviceMatchers) { | ||||
|  | ||||
|         describe("DeviceMatchers", function () { | ||||
|             var mockAgentService; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     'agentService', | ||||
|                     [ | ||||
|                         'isMobile', | ||||
|                         'isPhone', | ||||
|                         'isTablet', | ||||
|                         'isPortrait', | ||||
|                         'isLandscape', | ||||
|                         'isTouch' | ||||
|                     ] | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("detects when a device is a desktop device", function () { | ||||
|                 mockAgentService.isMobile.and.returnValue(false); | ||||
|                 expect(DeviceMatchers.desktop(mockAgentService)) | ||||
|                     .toBe(true); | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 expect(DeviceMatchers.desktop(mockAgentService)) | ||||
|                     .toBe(false); | ||||
|             }); | ||||
|  | ||||
|             function method(deviceType) { | ||||
|                 return "is" + deviceType[0].toUpperCase() + deviceType.slice(1); | ||||
|             } | ||||
|  | ||||
|             [ | ||||
|                 "mobile", | ||||
|                 "phone", | ||||
|                 "tablet", | ||||
|                 "landscape", | ||||
|                 "portrait", | ||||
|                 "landscape", | ||||
|                 "touch" | ||||
|             ].forEach(function (deviceType) { | ||||
|                 it("detects when a device is a " + deviceType + " device", function () { | ||||
|                     mockAgentService[method(deviceType)].and.returnValue(true); | ||||
|                     expect(DeviceMatchers[deviceType](mockAgentService)) | ||||
|                         .toBe(true); | ||||
|                     mockAgentService[method(deviceType)].and.returnValue(false); | ||||
|                     expect(DeviceMatchers[deviceType](mockAgentService)) | ||||
|                         .toBe(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,168 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['../src/MCTDevice'], | ||||
|     function (MCTDevice) { | ||||
|  | ||||
|         var JQLITE_METHODS = ['replaceWith']; | ||||
|  | ||||
|         describe("The mct-device directive", function () { | ||||
|             var mockAgentService, | ||||
|                 mockTransclude, | ||||
|                 mockElement, | ||||
|                 mockClone, | ||||
|                 testAttrs, | ||||
|                 directive; | ||||
|  | ||||
|             function link() { | ||||
|                 directive.link(null, mockElement, testAttrs, null, mockTransclude); | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     "agentService", | ||||
|                     ["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"] | ||||
|                 ); | ||||
|                 mockTransclude = jasmine.createSpy("$transclude"); | ||||
|                 mockElement = jasmine.createSpyObj(name, JQLITE_METHODS); | ||||
|                 mockClone = jasmine.createSpyObj(name, JQLITE_METHODS); | ||||
|  | ||||
|                 mockTransclude.and.callFake(function (fn) { | ||||
|                     fn(mockClone); | ||||
|                 }); | ||||
|  | ||||
|                 // Look desktop-like by default | ||||
|                 mockAgentService.isLandscape.and.returnValue(true); | ||||
|  | ||||
|                 testAttrs = {}; | ||||
|  | ||||
|                 directive = new MCTDevice(mockAgentService); | ||||
|             }); | ||||
|  | ||||
|             function expectInclusion() { | ||||
|                 expect(mockElement.replaceWith) | ||||
|                     .toHaveBeenCalledWith(mockClone); | ||||
|             } | ||||
|  | ||||
|             function expectExclusion() { | ||||
|                 expect(mockElement.replaceWith).not.toHaveBeenCalled(); | ||||
|             } | ||||
|  | ||||
|             it("is applicable at the attribute level", function () { | ||||
|                 expect(directive.restrict).toEqual("A"); | ||||
|             }); | ||||
|  | ||||
|             it("transcludes at the element level", function () { | ||||
|                 expect(directive.transclude).toEqual('element'); | ||||
|             }); | ||||
|  | ||||
|             it("has a greater priority number than ng-if", function () { | ||||
|                 expect(directive.priority > 600).toBeTruthy(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for mobile devices", function () { | ||||
|                 testAttrs.mctDevice = "mobile"; | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for tablet devices", function () { | ||||
|                 testAttrs.mctDevice = "tablet"; | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isTablet.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for phone devices", function () { | ||||
|                 testAttrs.mctDevice = "phone"; | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isPhone.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for desktop devices", function () { | ||||
|                 testAttrs.mctDevice = "desktop"; | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isMobile.and.returnValue(false); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for portrait orientation", function () { | ||||
|                 testAttrs.mctDevice = "portrait"; | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isPortrait.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("restricts element inclusion for landscape orientation", function () { | ||||
|                 testAttrs.mctDevice = "landscape"; | ||||
|                 mockAgentService.isLandscape.and.returnValue(false); | ||||
|                 mockAgentService.isPortrait.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isLandscape.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|  | ||||
|             it("allows multiple device characteristics to be requested", function () { | ||||
|                 // Won't try to test every permutation here, just | ||||
|                 // make sure the multi-characteristic feature has support. | ||||
|                 testAttrs.mctDevice = "portrait mobile"; | ||||
|                 link(); | ||||
|                 // Neither portrait nor mobile, not called | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isPortrait.and.returnValue(true); | ||||
|                 link(); | ||||
|  | ||||
|                 // Was portrait, but not mobile, so no | ||||
|                 expectExclusion(); | ||||
|  | ||||
|                 mockAgentService.isMobile.and.returnValue(true); | ||||
|                 link(); | ||||
|                 expectInclusion(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -379,7 +379,7 @@ define([ | ||||
|                     { | ||||
|                         "name": "Math.uuid.js", | ||||
|                         "version": "1.4.7", | ||||
|                         "description": "Unique identifer generation (code adapted.)", | ||||
|                         "description": "Unique identifier generation (code adapted.)", | ||||
|                         "author": "Robert Kieffer", | ||||
|                         "website": "https://github.com/broofa/node-uuid", | ||||
|                         "copyright": "Copyright (c) 2010-2012 Robert Kieffer", | ||||
|   | ||||
| @@ -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", | ||||
| @@ -186,70 +154,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "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(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -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)); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -44,9 +44,11 @@ define( | ||||
|                         setText(result.name); | ||||
|                         scope.ngModel[scope.field] = result; | ||||
|                         control.$setValidity("file-input", true); | ||||
|                         scope.$digest(); | ||||
|                     }, function () { | ||||
|                         setText('Select File'); | ||||
|                         control.$setValidity("file-input", false); | ||||
|                         scope.$digest(); | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -30,8 +30,8 @@ define([ | ||||
|  | ||||
|     return function ImportExportPlugin() { | ||||
|         return function (openmct) { | ||||
|             ExportAsJSONAction.appliesTo = function (context) { | ||||
|                 return openmct.$injector.get('policyService') | ||||
|             ExportAsJSONAction.prototype.appliesTo = function (context) { | ||||
|                 return this.openmct.$injector.get('policyService') | ||||
|                     .allow("creation", context.domainObject.getCapability("type") | ||||
|                     ); | ||||
|             }; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ define( | ||||
|     ], | ||||
|     function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) { | ||||
|  | ||||
|         xdescribe("The export JSON action", function () { | ||||
|         describe("The export JSON action", function () { | ||||
|  | ||||
|             var context, | ||||
|                 action, | ||||
| @@ -102,7 +102,7 @@ define( | ||||
|                 expect(action).toBeDefined(); | ||||
|             }); | ||||
|  | ||||
|             it("doesn't export non-creatable objects in tree", function () { | ||||
|             xit("doesn't export non-creatable objects in tree", function () { | ||||
|                 var nonCreatableType = { | ||||
|                     hasFeature: | ||||
|                         function (feature) { | ||||
| @@ -149,7 +149,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("can export self-containing objects", function () { | ||||
|             xit("can export self-containing objects", function () { | ||||
|                 var parent = domainObjectFactory({ | ||||
|                     name: 'parent', | ||||
|                     model: { | ||||
| @@ -191,7 +191,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("exports links to external objects as new objects", function () { | ||||
|             xit("exports links to external objects as new objects", function () { | ||||
|                 var parent = domainObjectFactory({ | ||||
|                     name: 'parent', | ||||
|                     model: { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ define( | ||||
|     ], | ||||
|     function (ImportAsJSONAction, domainObjectFactory) { | ||||
|  | ||||
|         xdescribe("The import JSON action", function () { | ||||
|         describe("The import JSON action", function () { | ||||
|  | ||||
|             var context = {}; | ||||
|             var action, | ||||
| @@ -146,7 +146,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("can import self-containing objects", function () { | ||||
|             xit("can import self-containing objects", function () { | ||||
|                 var compDomainObject = domainObjectFactory({ | ||||
|                     name: 'compObject', | ||||
|                     model: { name: 'compObject'}, | ||||
| @@ -198,7 +198,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("assigns new ids to each imported object", function () { | ||||
|             xit("assigns new ids to each imported object", function () { | ||||
|                 dialogService.getUserInput.and.returnValue(Promise.resolve( | ||||
|                     { | ||||
|                         selectFile: { | ||||
|   | ||||
| @@ -47,7 +47,7 @@ define( | ||||
|          * @param $interval Angular's $interval service | ||||
|          * @param {string} space the name of the persistence space being served | ||||
|          * @param {string} root the root of the path to ElasticSearch | ||||
|          * @param {stirng} path the path to domain objects within ElasticSearch | ||||
|          * @param {string} path the path to domain objects within ElasticSearch | ||||
|          */ | ||||
|         function ElasticPersistenceProvider($http, $q, space, root, path) { | ||||
|             this.spaces = [space]; | ||||
|   | ||||
| @@ -136,7 +136,7 @@ define([ | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name conductor | ||||
|          */ | ||||
|         this.time = new api.TimeAPI(); | ||||
|         this.time = new api.TimeAPI(this); | ||||
|  | ||||
|         /** | ||||
|          * An interface for interacting with the composition of domain objects. | ||||
| @@ -263,6 +263,7 @@ define([ | ||||
|         // Plugins that are installed by default | ||||
|  | ||||
|         this.install(this.plugins.Plot()); | ||||
|         this.install(this.plugins.Chart()); | ||||
|         this.install(this.plugins.TelemetryTable.default()); | ||||
|         this.install(PreviewPlugin.default()); | ||||
|         this.install(LegacyIndicatorsPlugin()); | ||||
| @@ -287,6 +288,7 @@ define([ | ||||
|         this.install(this.plugins.ViewLargeAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.NonEditableFolder()); | ||||
|         this.install(this.plugins.DeviceClassifier()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
| @@ -81,14 +81,8 @@ define([ | ||||
|                     return models; | ||||
|                 } | ||||
|  | ||||
|                 return this.apiFetch(missingIds) | ||||
|                     .then(function (apiResults) { | ||||
|                         Object.keys(apiResults).forEach(function (k) { | ||||
|                             models[k] = apiResults[k]; | ||||
|                         }); | ||||
|  | ||||
|                         return models; | ||||
|                     }); | ||||
|                 //Temporary fix for missing models - don't retry using this.apiFetch | ||||
|                 return models; | ||||
|             }.bind(this)); | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -60,9 +60,7 @@ class ActionsAPI extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     _getCachedActionCollection(objectPath, view) { | ||||
|         let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
|         return cachedActionCollection; | ||||
|         return this._actionCollections.get(view); | ||||
|     } | ||||
|  | ||||
|     _newActionCollection(objectPath, view, skipEnvironmentObservers) { | ||||
| @@ -112,7 +110,7 @@ class ActionsAPI extends EventEmitter { | ||||
|         return actionsObject; | ||||
|     } | ||||
|  | ||||
|     _groupAndSortActions(actionsArray) { | ||||
|     _groupAndSortActions(actionsArray = []) { | ||||
|         if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { | ||||
|             actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); | ||||
|         } | ||||
|   | ||||
| @@ -46,7 +46,7 @@ define([ | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
|         TimeAPI: TimeAPI.default, | ||||
|         ObjectAPI: ObjectAPI, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         TypeRegistry: TypeRegistry, | ||||
|   | ||||
| @@ -42,7 +42,7 @@ import EventEmitter from 'EventEmitter'; | ||||
|  * | ||||
|  * @typedef {object} NotificationModel | ||||
|  * @property {string} message The message to be displayed by the notification | ||||
|  * @property {number | 'unknown'} [progress] The progres of some ongoing task. Should be a number between 0 and 100, or | ||||
|  * @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or | ||||
|  * with the string literal 'unknown'. | ||||
|  * @property {string} [progressText] A message conveying progress of some ongoing task. | ||||
|  | ||||
| @@ -98,7 +98,7 @@ export default class NotificationAPI extends EventEmitter { | ||||
|      * Present an alert to the user. | ||||
|      * @param {string} message The message to display to the user. | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      autoDismissTimeout: {number} in milliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
| @@ -119,7 +119,7 @@ export default class NotificationAPI extends EventEmitter { | ||||
|      * Present an error message to the user | ||||
|      * @param {string} message | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      autoDismissTimeout: {number} in milliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/api/objects/ConflictError.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/api/objects/ConflictError.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export default class ConflictError extends Error { | ||||
| } | ||||
| @@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry'; | ||||
| import RootObjectProvider from './RootObjectProvider'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import InterceptorRegistry from './InterceptorRegistry'; | ||||
| import ConflictError from './ConflictError'; | ||||
|  | ||||
| /** | ||||
|  * Utilities for loading, saving, and manipulating domain objects. | ||||
| @@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry'; | ||||
|  */ | ||||
|  | ||||
| function ObjectAPI(typeRegistry, openmct) { | ||||
|     this.openmct = openmct; | ||||
|     this.typeRegistry = typeRegistry; | ||||
|     this.eventEmitter = new EventEmitter(); | ||||
|     this.providers = {}; | ||||
| @@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) { | ||||
|     this.interceptorRegistry = new InterceptorRegistry(); | ||||
|  | ||||
|     this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan']; | ||||
|  | ||||
|     this.errors = { | ||||
|         Conflict: ConflictError | ||||
|     }; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|  | ||||
|     let objectPromise = provider.get(identifier, abortSignal).then(result => { | ||||
|         delete this.cache[keystring]; | ||||
|  | ||||
|         result = this.applyGetInterceptors(identifier, result); | ||||
|  | ||||
|         return result; | ||||
|     }).catch((result) => { | ||||
|         console.warn(`Failed to retrieve ${keystring}:`, result); | ||||
|  | ||||
|         delete this.cache[keystring]; | ||||
|  | ||||
|         result = this.applyGetInterceptors(identifier); | ||||
|  | ||||
|         return result; | ||||
|     }); | ||||
|  | ||||
| @@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) { | ||||
| ObjectAPI.prototype.save = function (domainObject) { | ||||
|     let provider = this.getProvider(domainObject.identifier); | ||||
|     let savedResolve; | ||||
|     let savedReject; | ||||
|     let result; | ||||
|  | ||||
|     if (!this.isPersistable(domainObject.identifier)) { | ||||
| @@ -294,14 +310,18 @@ ObjectAPI.prototype.save = function (domainObject) { | ||||
|     } else { | ||||
|         const persistedTime = Date.now(); | ||||
|         if (domainObject.persisted === undefined) { | ||||
|             result = new Promise((resolve) => { | ||||
|             result = new Promise((resolve, reject) => { | ||||
|                 savedResolve = resolve; | ||||
|                 savedReject = reject; | ||||
|             }); | ||||
|             domainObject.persisted = persistedTime; | ||||
|             provider.create(domainObject).then((response) => { | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 savedResolve(response); | ||||
|             }); | ||||
|             provider.create(domainObject) | ||||
|                 .then((response) => { | ||||
|                     this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                     savedResolve(response); | ||||
|                 }).catch((error) => { | ||||
|                     savedReject(error); | ||||
|                 }); | ||||
|         } else { | ||||
|             domainObject.persisted = persistedTime; | ||||
|             this.mutate(domainObject, 'persisted', persistedTime); | ||||
| @@ -358,6 +378,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) { | ||||
|     return domainObject; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Return relative url path from a given object path | ||||
|  * eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/.... | ||||
|  * @param {Array} objectPath | ||||
|  * @returns {string} relative url for object | ||||
|  */ | ||||
| ObjectAPI.prototype.getRelativePath = function (objectPath) { | ||||
|     return objectPath | ||||
|         .map(p => this.makeKeyString(p.identifier)) | ||||
|         .reverse() | ||||
|         .join('/') | ||||
|     ; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Modify a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to mutate | ||||
|   | ||||
| @@ -10,28 +10,37 @@ const cssClasses = { | ||||
| }; | ||||
|  | ||||
| class Overlay extends EventEmitter { | ||||
|     constructor(options) { | ||||
|     constructor({ | ||||
|         buttons, | ||||
|         autoHide = true, | ||||
|         dismissable = true, | ||||
|         element, | ||||
|         onDestroy, | ||||
|         size | ||||
|     } = {}) { | ||||
|         super(); | ||||
|  | ||||
|         this.dismissable = options.dismissable !== false; | ||||
|         this.container = document.createElement('div'); | ||||
|         this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]); | ||||
|         this.container.classList.add('l-overlay-wrapper', cssClasses[size]); | ||||
|  | ||||
|         this.autoHide = autoHide; | ||||
|         this.dismissable = dismissable !== false; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 dismiss: this.dismiss.bind(this), | ||||
|                 element: options.element, | ||||
|                 buttons: options.buttons, | ||||
|                 dismissable: this.dismissable | ||||
|             }, | ||||
|             components: { | ||||
|                 OverlayComponent: OverlayComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 dismiss: this.dismiss.bind(this), | ||||
|                 element, | ||||
|                 buttons, | ||||
|                 dismissable: this.dismissable | ||||
|             }, | ||||
|             template: '<overlay-component></overlay-component>' | ||||
|         }); | ||||
|  | ||||
|         if (options.onDestroy) { | ||||
|             this.once('destroy', options.onDestroy); | ||||
|         if (onDestroy) { | ||||
|             this.once('destroy', onDestroy); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,10 @@ class OverlayAPI { | ||||
|      */ | ||||
|     showOverlay(overlay) { | ||||
|         if (this.activeOverlays.length) { | ||||
|             this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible'); | ||||
|             const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1]; | ||||
|             if (previousOverlay.autoHide) { | ||||
|                 previousOverlay.container.classList.add('invisible'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.activeOverlays.push(overlay); | ||||
| @@ -60,7 +63,7 @@ class OverlayAPI { | ||||
|      * A description of option properties that can be passed into the overlay | ||||
|      * @typedef options | ||||
|         * @property {object} element DOMElement that is to be inserted/shown on the overlay | ||||
|         * @property {string} size prefered size of the overlay (large, small, fit) | ||||
|         * @property {string} size preferred size of the overlay (large, small, fit) | ||||
|         * @property {array} buttons optional button objects with label and callback properties | ||||
|         * @property {function} onDestroy callback to be called when overlay is destroyed | ||||
|         * @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|         ></button> | ||||
|         <div | ||||
|             ref="element" | ||||
|             class="c-overlay__contents" | ||||
|             class="c-overlay__contents js-notebook-snapshot-item-wrapper" | ||||
|             tabindex="0" | ||||
|         ></div> | ||||
|         <div | ||||
|   | ||||
| @@ -20,6 +20,8 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
|  | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
| @@ -273,6 +275,28 @@ define([ | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request telemetry collection for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
|      * (start, end, etc.), sort order, and strategies for retrieving | ||||
|      * telemetry (aggregation, latest available, etc.). | ||||
|      * | ||||
|      * @method requestCollection | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the object | ||||
|      *        which has associated telemetry | ||||
|      * @param {module:openmct.TelemetryAPI~TelemetryRequest} options | ||||
|      *        options for this telemetry collection request | ||||
|      * @returns {TelemetryCollection} a TelemetryCollection instance | ||||
|      */ | ||||
|     TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { | ||||
|         return new TelemetryCollection( | ||||
|             this.openmct, | ||||
|             domainObject, | ||||
|             options | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request historical telemetry for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
| @@ -459,6 +483,10 @@ define([ | ||||
|      * @returns {Object<String, {TelemetryValueFormatter}>} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatMap = function (metadata) { | ||||
|         if (!metadata) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         if (!this.formatMapCache.has(metadata)) { | ||||
|             const formatMap = metadata.values().reduce(function (map, valueMetadata) { | ||||
|                 map[valueMetadata.key] = this.getValueFormatter(valueMetadata); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										403
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										403
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,403 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 _ from 'lodash'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| const ERRORS = { | ||||
|     TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.', | ||||
|     LOADED: 'Telemetry Collection has already been loaded.' | ||||
| }; | ||||
|  | ||||
| /** Class representing a Telemetry Collection. */ | ||||
|  | ||||
| export class TelemetryCollection extends EventEmitter { | ||||
|     /** | ||||
|      * Creates a Telemetry Collection | ||||
|      * | ||||
|      * @param  {object} openmct - Openm MCT | ||||
|      * @param  {object} domainObject - Domain Object to user for telemetry collection | ||||
|      * @param  {object} options - Any options passed in for request/subscribe | ||||
|      */ | ||||
|     constructor(openmct, domainObject, options) { | ||||
|         super(); | ||||
|  | ||||
|         this.loaded = false; | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|         this.parseTime = undefined; | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
|         this.unsubscribe = undefined; | ||||
|         this.options = options; | ||||
|         this.pageState = undefined; | ||||
|         this.lastBounds = undefined; | ||||
|         this.requestAbort = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This will start the requests for historical and realtime data, | ||||
|      * as well as setting up initial values and watchers | ||||
|      */ | ||||
|     load() { | ||||
|         if (this.loaded) { | ||||
|             this._error(ERRORS.LOADED); | ||||
|         } | ||||
|  | ||||
|         this._setTimeSystem(this.openmct.time.timeSystem()); | ||||
|         this.lastBounds = this.openmct.time.bounds(); | ||||
|  | ||||
|         this._watchBounds(); | ||||
|         this._watchTimeSystem(); | ||||
|  | ||||
|         this._requestHistoricalTelemetry(); | ||||
|         this._initiateSubscriptionTelemetry(); | ||||
|  | ||||
|         this.loaded = true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * can/should be called by the requester of the telemetry collection | ||||
|      * to remove any listeners | ||||
|      */ | ||||
|     destroy() { | ||||
|         if (this.requestAbort) { | ||||
|             this.requestAbort.abort(); | ||||
|         } | ||||
|  | ||||
|         this._unwatchBounds(); | ||||
|         this._unwatchTimeSystem(); | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|  | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This will start the requests for historical and realtime data, | ||||
|      * as well as setting up initial values and watchers | ||||
|      */ | ||||
|     getAll() { | ||||
|         return this.boundedTelemetry; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If a historical provider exists, then historical requests will be made | ||||
|      * @private | ||||
|      */ | ||||
|     async _requestHistoricalTelemetry() { | ||||
|         let options = { ...this.options }; | ||||
|         let historicalProvider; | ||||
|  | ||||
|         this.openmct.telemetry.standardizeRequestOptions(options); | ||||
|         historicalProvider = this.openmct.telemetry. | ||||
|             findRequestProvider(this.domainObject, options); | ||||
|  | ||||
|         if (!historicalProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let historicalData; | ||||
|  | ||||
|         options.onPartialResponse = this._processNewTelemetry.bind(this); | ||||
|  | ||||
|         try { | ||||
|             if (this.requestAbort) { | ||||
|                 this.requestAbort.abort(); | ||||
|             } | ||||
|  | ||||
|             this.requestAbort = new AbortController(); | ||||
|             options.signal = this.requestAbort.signal; | ||||
|             this.emit('requestStarted'); | ||||
|             historicalData = await historicalProvider.request(this.domainObject, options); | ||||
|         } catch (error) { | ||||
|             if (error.name !== 'AbortError') { | ||||
|                 console.error('Error requesting telemetry data...'); | ||||
|                 this._error(error); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.emit('requestEnded'); | ||||
|         this.requestAbort = undefined; | ||||
|  | ||||
|         this._processNewTelemetry(historicalData); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This uses the built in subscription function from Telemetry API | ||||
|      * @private | ||||
|      */ | ||||
|     _initiateSubscriptionTelemetry() { | ||||
|  | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|  | ||||
|         this.unsubscribe = this.openmct.telemetry | ||||
|             .subscribe( | ||||
|                 this.domainObject, | ||||
|                 datum => this._processNewTelemetry(datum), | ||||
|                 this.options | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Filter any new telemetry (add/page, historical, subscription) based on | ||||
|      * time bounds and dupes | ||||
|      * | ||||
|      * @param  {(Object|Object[])} telemetryData - telemetry data object or | ||||
|      * array of telemetry data objects | ||||
|      * @private | ||||
|      */ | ||||
|     _processNewTelemetry(telemetryData) { | ||||
|         if (telemetryData === undefined) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; | ||||
|         let parsedValue; | ||||
|         let beforeStartOfBounds; | ||||
|         let afterEndOfBounds; | ||||
|         let added = []; | ||||
|  | ||||
|         for (let datum of data) { | ||||
|             parsedValue = this.parseTime(datum); | ||||
|             beforeStartOfBounds = parsedValue < this.lastBounds.start; | ||||
|             afterEndOfBounds = parsedValue > this.lastBounds.end; | ||||
|  | ||||
|             if (!afterEndOfBounds && !beforeStartOfBounds) { | ||||
|                 let isDuplicate = false; | ||||
|                 let startIndex = this._sortedIndex(datum); | ||||
|                 let endIndex = undefined; | ||||
|  | ||||
|                 // dupe check | ||||
|                 if (startIndex !== this.boundedTelemetry.length) { | ||||
|                     endIndex = _.sortedLastIndexBy( | ||||
|                         this.boundedTelemetry, | ||||
|                         datum, | ||||
|                         boundedDatum => this.parseTime(boundedDatum) | ||||
|                     ); | ||||
|  | ||||
|                     if (endIndex > startIndex) { | ||||
|                         let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); | ||||
|                         isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); | ||||
|                     } | ||||
|                 } else if (startIndex === this.boundedTelemetry.length) { | ||||
|                     isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]); | ||||
|                 } | ||||
|  | ||||
|                 if (!isDuplicate) { | ||||
|                     let index = endIndex || startIndex; | ||||
|  | ||||
|                     this.boundedTelemetry.splice(index, 0, datum); | ||||
|                     added.push(datum); | ||||
|                 } | ||||
|  | ||||
|             } else if (afterEndOfBounds) { | ||||
|                 this.futureBuffer.push(datum); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (added.length) { | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Finds the correct insertion point for the given telemetry datum. | ||||
|      * Leverages lodash's `sortedIndexBy` function which implements a binary search. | ||||
|      * @private | ||||
|      */ | ||||
|     _sortedIndex(datum) { | ||||
|         if (this.boundedTelemetry.length === 0) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         let parsedValue = this.parseTime(datum); | ||||
|         let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]); | ||||
|  | ||||
|         if (parsedValue > lastValue || parsedValue === lastValue) { | ||||
|             return this.boundedTelemetry.length; | ||||
|         } else { | ||||
|             return _.sortedIndexBy( | ||||
|                 this.boundedTelemetry, | ||||
|                 datum, | ||||
|                 boundedDatum => this.parseTime(boundedDatum) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * when the start time, end time, or both have been updated. | ||||
|      * data could be added OR removed here we update the current | ||||
|      * bounded telemetry | ||||
|      * | ||||
|      * @param  {TimeConductorBounds} bounds The newly updated bounds | ||||
|      * @param  {boolean} [tick] `true` if the bounds update was due to | ||||
|      * a "tick" event (ie. was an automatic update), false otherwise. | ||||
|      * @private | ||||
|      */ | ||||
|     _bounds(bounds, isTick) { | ||||
|         let startChanged = this.lastBounds.start !== bounds.start; | ||||
|         let endChanged = this.lastBounds.end !== bounds.end; | ||||
|  | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (isTick) { | ||||
|             // need to check futureBuffer and need to check | ||||
|             // if anything has fallen out of bounds | ||||
|             let startIndex = 0; | ||||
|             let endIndex = 0; | ||||
|  | ||||
|             let discarded = []; | ||||
|             let added = []; | ||||
|             let testDatum = {}; | ||||
|  | ||||
|             if (startChanged) { | ||||
|                 testDatum[this.timeKey] = bounds.start; | ||||
|                 // Calculate the new index of the first item within the bounds | ||||
|                 startIndex = _.sortedIndexBy( | ||||
|                     this.boundedTelemetry, | ||||
|                     testDatum, | ||||
|                     datum => this.parseTime(datum) | ||||
|                 ); | ||||
|                 discarded = this.boundedTelemetry.splice(0, startIndex); | ||||
|             } | ||||
|  | ||||
|             if (endChanged) { | ||||
|                 testDatum[this.timeKey] = bounds.end; | ||||
|                 // Calculate the new index of the last item in bounds | ||||
|                 endIndex = _.sortedLastIndexBy( | ||||
|                     this.futureBuffer, | ||||
|                     testDatum, | ||||
|                     datum => this.parseTime(datum) | ||||
|                 ); | ||||
|                 added = this.futureBuffer.splice(0, endIndex); | ||||
|                 this.boundedTelemetry = [...this.boundedTelemetry, ...added]; | ||||
|             } | ||||
|  | ||||
|             if (discarded.length > 0) { | ||||
|                 this.emit('remove', discarded); | ||||
|             } | ||||
|  | ||||
|             if (added.length > 0) { | ||||
|                 this.emit('add', added); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             // user bounds change, reset | ||||
|             this._reset(); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * whenever the time system is updated need to update related values in | ||||
|      * the Telemetry Collection and reset the telemetry collection | ||||
|      * | ||||
|      * @param  {TimeSystem} timeSystem - the value of the currently applied | ||||
|      * Time System | ||||
|      * @private | ||||
|      */ | ||||
|     _setTimeSystem(timeSystem) { | ||||
|         let domains = this.metadata.valuesForHints(['domain']); | ||||
|         let domain = domains.find((d) => d.key === timeSystem.key); | ||||
|  | ||||
|         if (domain === undefined) { | ||||
|             this._error(ERRORS.TIMESYSTEM_KEY); | ||||
|         } | ||||
|  | ||||
|         // timeKey is used to create a dummy datum used for sorting | ||||
|         this.timeKey = domain.source; // this defaults to key if no source is set | ||||
|         let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; | ||||
|         let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|         this.parseTime = (datum) => { | ||||
|             return valueFormatter.parse(datum); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     _setTimeSystemAndFetchData(timeSystem) { | ||||
|         this._setTimeSystem(timeSystem); | ||||
|         this._reset(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reset the telemetry data of the collection, and re-request | ||||
|      * historical telemetry | ||||
|      * @private | ||||
|      * | ||||
|      * @todo handle subscriptions more granually | ||||
|      */ | ||||
|     _reset() { | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|         this.emit('clear'); | ||||
|  | ||||
|         this._requestHistoricalTelemetry(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * adds the _bounds callback to the 'bounds' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _watchBounds() { | ||||
|         this.openmct.time.on('bounds', this._bounds, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * removes the _bounds callback from the 'bounds' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _unwatchBounds() { | ||||
|         this.openmct.time.off('bounds', this._bounds, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _watchTimeSystem() { | ||||
|         this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _unwatchTimeSystem() { | ||||
|         this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * will throw a new Error, for passed in message | ||||
|      * @param  {string} message Message describing the error | ||||
|      * @private | ||||
|      */ | ||||
|     _error(message) { | ||||
|         throw new Error(message); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										106
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 TimeContext from "./TimeContext"; | ||||
|  | ||||
| /** | ||||
|  * The GlobalContext handles getting and setting time of the openmct application in general. | ||||
|  * Views will use this context unless they specify an alternate/independent time context | ||||
|  */ | ||||
| class GlobalTimeContext extends TimeContext { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         //The Time Of Interest | ||||
|         this.toi = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     bounds(newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             super.bounds.call(this, ...arguments); | ||||
|             // If a bounds change results in a TOI outside of the current | ||||
|             // bounds, unset it | ||||
|             if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|                 this.timeOfInterest(undefined); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @private | ||||
|      * @param {number} timestamp A time from which bounds will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     tick(timestamp) { | ||||
|         super.tick.call(this, ...arguments); | ||||
|  | ||||
|         // If a bounds change results in a TOI outside of the current | ||||
|         // bounds, unset it | ||||
|         if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) { | ||||
|             this.timeOfInterest(undefined); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the Time of Interest. The Time of Interest is a single point | ||||
|      * in time, and constitutes the temporal focus of application views. It can | ||||
|      * be manipulated by the user from the time conductor or from other views. | ||||
|      * The time of interest can effectively be unset by assigning a value of | ||||
|      * 'undefined'. | ||||
|      * @fires module:openmct.TimeAPI~timeOfInterest | ||||
|      * @param newTOI | ||||
|      * @returns {number} the current time of interest | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeOfInterest | ||||
|      */ | ||||
|     timeOfInterest(newTOI) { | ||||
|         if (arguments.length > 0) { | ||||
|             this.toi = newTOI; | ||||
|             /** | ||||
|              * The Time of Interest has moved. | ||||
|              * @event timeOfInterest | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {number} Current time of interest | ||||
|              */ | ||||
|             this.emit('timeOfInterest', this.toi); | ||||
|         } | ||||
|  | ||||
|         return this.toi; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default GlobalTimeContext; | ||||
							
								
								
									
										94
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 TimeContext from "./TimeContext"; | ||||
|  | ||||
| /** | ||||
|  * The IndependentTimeContext handles getting and setting time of the openmct application in general. | ||||
|  * Views will use the GlobalTimeContext unless they specify an alternate/independent time context here. | ||||
|  */ | ||||
| class IndependentTimeContext extends TimeContext { | ||||
|     constructor(globalTimeContext, key) { | ||||
|         super(); | ||||
|         this.key = key; | ||||
|  | ||||
|         this.globalTimeContext = globalTimeContext; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} keyOrClock The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      */ | ||||
|     clock(keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.globalTimeContext.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.globalTimeContext.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default IndependentTimeContext; | ||||
| @@ -20,51 +20,35 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['EventEmitter'], function (EventEmitter) { | ||||
|  | ||||
|     /** | ||||
|      * The public API for setting and querying the temporal state of the | ||||
|      * application. The concept of time is integral to Open MCT, and at least | ||||
|      * one {@link TimeSystem}, as well as some default time bounds must be | ||||
|      * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
|      * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
|      * | ||||
|      * Time-sensitive views will typically respond to changes to bounds or other | ||||
|      * properties of the time conductor and update the data displayed based on | ||||
|      * the temporal state of the application. The current time bounds are also | ||||
|      * used in queries for historical data. | ||||
|      * | ||||
|      * The TimeAPI extends the EventEmitter class. A number of events are | ||||
|      * fired when properties of the time conductor change, which are documented | ||||
|      * below. | ||||
|      * | ||||
|      * @interface | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     function TimeAPI() { | ||||
|         EventEmitter.call(this); | ||||
|  | ||||
|         //The Time System | ||||
|         this.system = undefined; | ||||
|         //The Time Of Interest | ||||
|         this.toi = undefined; | ||||
|  | ||||
|         this.boundsVal = { | ||||
|             start: undefined, | ||||
|             end: undefined | ||||
|         }; | ||||
|  | ||||
|         this.timeSystems = new Map(); | ||||
|         this.clocks = new Map(); | ||||
|         this.activeClock = undefined; | ||||
|         this.offsets = undefined; | ||||
|  | ||||
|         this.tick = this.tick.bind(this); | ||||
| import GlobalTimeContext from "./GlobalTimeContext"; | ||||
| import IndependentTimeContext from "@/api/time/IndependentTimeContext"; | ||||
|  | ||||
| /** | ||||
| * The public API for setting and querying the temporal state of the | ||||
| * application. The concept of time is integral to Open MCT, and at least | ||||
| * one {@link TimeSystem}, as well as some default time bounds must be | ||||
| * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
| * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
| * | ||||
| * Time-sensitive views will typically respond to changes to bounds or other | ||||
| * properties of the time conductor and update the data displayed based on | ||||
| * the temporal state of the application. The current time bounds are also | ||||
| * used in queries for historical data. | ||||
| * | ||||
| * The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are | ||||
| * fired when properties of the time conductor change, which are documented | ||||
| * below. | ||||
| * | ||||
| * @interface | ||||
| * @memberof module:openmct | ||||
| */ | ||||
| class TimeAPI extends GlobalTimeContext { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|         this.openmct = openmct; | ||||
|         this.independentContexts = new Map(); | ||||
|     } | ||||
|  | ||||
|     TimeAPI.prototype = Object.create(EventEmitter.prototype); | ||||
|  | ||||
|     /** | ||||
|      * A TimeSystem provides meaning to the values returned by the TimeAPI. Open | ||||
|      * MCT supports multiple different types of time values, although all are | ||||
| @@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @param {TimeSystem} timeSystem A time system object. | ||||
|      */ | ||||
|     TimeAPI.prototype.addTimeSystem = function (timeSystem) { | ||||
|     addTimeSystem(timeSystem) { | ||||
|         this.timeSystems.set(timeSystem.key, timeSystem); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {TimeSystem[]} | ||||
|      */ | ||||
|     TimeAPI.prototype.getAllTimeSystems = function () { | ||||
|     getAllTimeSystems() { | ||||
|         return Array.from(this.timeSystems.values()); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clocks provide a timing source that is used to | ||||
| @@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @param {Clock} clock | ||||
|      */ | ||||
|     TimeAPI.prototype.addClock = function (clock) { | ||||
|     addClock(clock) { | ||||
|         this.clocks.set(clock.key, clock); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @returns {Clock[]} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      */ | ||||
|     TimeAPI.prototype.getAllClocks = function () { | ||||
|     getAllClocks() { | ||||
|         return Array.from(this.clocks.values()); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validate the given bounds. This can be used for pre-validation of bounds, | ||||
|      * for example by views validating user inputs. | ||||
|      * @param {TimeBounds} bounds The start and end time of the conductor. | ||||
|      * @returns {string | true} A validation error, or true if valid | ||||
|      * Get or set an independent time context which follows the TimeAPI timeSystem, | ||||
|      * but with different offsets for a given domain object | ||||
|      * @param {key | string} key The identifier key of the domain object these offsets are set for | ||||
|      * @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates | ||||
|      * @param {key | string} clockKey the real time clock key currently in use | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      * @method addIndependentTimeContext | ||||
|      */ | ||||
|     TimeAPI.prototype.validateBounds = function (bounds) { | ||||
|         if ((bounds.start === undefined) | ||||
|             || (bounds.end === undefined) | ||||
|             || isNaN(bounds.start) | ||||
|             || isNaN(bounds.end) | ||||
|         ) { | ||||
|             return "Start and end must be specified as integer values"; | ||||
|         } else if (bounds.start > bounds.end) { | ||||
|             return "Specified start date exceeds end bound"; | ||||
|     addIndependentContext(key, value, clockKey) { | ||||
|         let timeContext = this.independentContexts.get(key); | ||||
|         if (!timeContext) { | ||||
|             timeContext = new IndependentTimeContext(this, key); | ||||
|             this.independentContexts.set(key, timeContext); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Validate the given offsets. This can be used for pre-validation of | ||||
|      * offsets, for example by views validating user inputs. | ||||
|      * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. | ||||
|      * @returns {string | true} A validation error, or true if valid | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      */ | ||||
|     TimeAPI.prototype.validateOffsets = function (offsets) { | ||||
|         if ((offsets.start === undefined) | ||||
|             || (offsets.end === undefined) | ||||
|             || isNaN(offsets.start) | ||||
|             || isNaN(offsets.end) | ||||
|         ) { | ||||
|             return "Start and end offsets must be specified as integer values"; | ||||
|         } else if (offsets.start >= offsets.end) { | ||||
|             return "Specified start offset must be < end offset"; | ||||
|         if (clockKey) { | ||||
|             timeContext.clock(clockKey, value); | ||||
|         } else { | ||||
|             timeContext.stopClock(); | ||||
|             timeContext.bounds(value); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }; | ||||
|         this.emit('timeContext', key); | ||||
|  | ||||
|     /** | ||||
|      * @typedef {Object} TimeBounds | ||||
|      * @property {number} start The start time displayed by the time conductor | ||||
|      * in ms since epoch. Epoch determined by currently active time system | ||||
|      * @property {number} end The end time displayed by the time conductor in ms | ||||
|      * since epoch. | ||||
|      * @memberof module:openmct.TimeAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     TimeAPI.prototype.bounds = function (newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             const validationResult = this.validateBounds(newBounds); | ||||
|             if (validationResult !== true) { | ||||
|                 throw new Error(validationResult); | ||||
|             } | ||||
|  | ||||
|             //Create a copy to avoid direct mutation of conductor bounds | ||||
|             this.boundsVal = JSON.parse(JSON.stringify(newBounds)); | ||||
|             /** | ||||
|              * The start time, end time, or both have been updated. | ||||
|              * @event bounds | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {TimeConductorBounds} bounds The newly updated bounds | ||||
|              * @property {boolean} [tick] `true` if the bounds update was due to | ||||
|              * a "tick" event (ie. was an automatic update), false otherwise. | ||||
|              */ | ||||
|             this.emit('bounds', this.boundsVal, false); | ||||
|  | ||||
|             // If a bounds change results in a TOI outside of the current | ||||
|             // bounds, unset it | ||||
|             if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|                 this.timeOfInterest(undefined); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get or set the time system of the TimeAPI. | ||||
|      * @param {TimeSystem | string} timeSystem | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds | ||||
|      * @fires module:openmct.TimeAPI~timeSystem | ||||
|      * @returns {TimeSystem} The currently applied time system | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeSystem | ||||
|      */ | ||||
|     TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) { | ||||
|         if (arguments.length >= 1) { | ||||
|             if (arguments.length === 1 && !this.activeClock) { | ||||
|                 throw new Error( | ||||
|                     "Must specify bounds when changing time system without " | ||||
|                     + "an active clock." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let timeSystem; | ||||
|  | ||||
|             if (timeSystemOrKey === undefined) { | ||||
|                 throw "Please provide a time system"; | ||||
|             } | ||||
|  | ||||
|             if (typeof timeSystemOrKey === 'string') { | ||||
|                 timeSystem = this.timeSystems.get(timeSystemOrKey); | ||||
|  | ||||
|                 if (timeSystem === undefined) { | ||||
|                     throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else if (typeof timeSystemOrKey === 'object') { | ||||
|                 timeSystem = timeSystemOrKey; | ||||
|  | ||||
|                 if (!this.timeSystems.has(timeSystem.key)) { | ||||
|                     throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else { | ||||
|                 throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; | ||||
|             } | ||||
|  | ||||
|             this.system = timeSystem; | ||||
|  | ||||
|             /** | ||||
|              * The time system used by the time | ||||
|              * conductor has changed. A change in Time System will always be | ||||
|              * followed by a bounds event specifying new query bounds. | ||||
|              * | ||||
|              * @event module:openmct.TimeAPI~timeSystem | ||||
|              * @property {TimeSystem} The value of the currently applied | ||||
|              * Time System | ||||
|              * */ | ||||
|             this.emit('timeSystem', this.system); | ||||
|             if (bounds) { | ||||
|                 this.bounds(bounds); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return this.system; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get or set the Time of Interest. The Time of Interest is a single point | ||||
|      * in time, and constitutes the temporal focus of application views. It can | ||||
|      * be manipulated by the user from the time conductor or from other views. | ||||
|      * The time of interest can effectively be unset by assigning a value of | ||||
|      * 'undefined'. | ||||
|      * @fires module:openmct.TimeAPI~timeOfInterest | ||||
|      * @param newTOI | ||||
|      * @returns {number} the current time of interest | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeOfInterest | ||||
|      */ | ||||
|     TimeAPI.prototype.timeOfInterest = function (newTOI) { | ||||
|         if (arguments.length > 0) { | ||||
|             this.toi = newTOI; | ||||
|             /** | ||||
|              * The Time of Interest has moved. | ||||
|              * @event timeOfInterest | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {number} Current time of interest | ||||
|              */ | ||||
|             this.emit('timeOfInterest', this.toi); | ||||
|         } | ||||
|  | ||||
|         return this.toi; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @private | ||||
|      * @param {number} timestamp A time from which boudns will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     TimeAPI.prototype.tick = function (timestamp) { | ||||
|         const newBounds = { | ||||
|             start: timestamp + this.offsets.start, | ||||
|             end: timestamp + this.offsets.end | ||||
|         return () => { | ||||
|             this.independentContexts.delete(key); | ||||
|             timeContext.emit('timeContext', key); | ||||
|         }; | ||||
|  | ||||
|         this.boundsVal = newBounds; | ||||
|         this.emit('bounds', this.boundsVal, true); | ||||
|  | ||||
|         // If a bounds change results in a TOI outside of the current | ||||
|         // bounds, unset it | ||||
|         if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|             this.timeOfInterest(undefined); | ||||
|         } | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      * Get the independent time context which follows the TimeAPI timeSystem, | ||||
|      * but with different offsets. | ||||
|      * @param {key | string} key The identifier key of the domain object these offsets | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getIndependentTimeContext | ||||
|      */ | ||||
|     TimeAPI.prototype.clock = function (keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     }; | ||||
|     getIndependentContext(key) { | ||||
|         return this.independentContexts.get(key); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ClockOffsets | ||||
|      * @property {number} start A time span relative to the current value of the | ||||
|      * ticking clock, from which start bounds will be calculated. This value must | ||||
|      * be < 0. When a clock is active, bounds will be calculated automatically | ||||
|      * based on the value provided by the clock, and the defined clock offsets. | ||||
|      * @property {number} end A time span relative to the current value of the | ||||
|      * ticking clock, from which end bounds will be calculated. This value must | ||||
|      * be >= 0. | ||||
|      * Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned. | ||||
|      * Otherwise, the global time context will be returned. | ||||
|      * @param { Array } objectPath The view's objectPath | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getContextForView | ||||
|      */ | ||||
|     /** | ||||
|      * Get or set the currently applied clock offsets. If no parameter is provided, | ||||
|      * the current value will be returned. If provided, the new value will be | ||||
|      * used as the new clock offsets. | ||||
|      * @param {ClockOffsets} offsets | ||||
|      * @returns {ClockOffsets} | ||||
|      */ | ||||
|     TimeAPI.prototype.clockOffsets = function (offsets) { | ||||
|         if (arguments.length > 0) { | ||||
|     getContextForView(objectPath = []) { | ||||
|         let timeContext = this; | ||||
|  | ||||
|             const validationResult = this.validateOffsets(offsets); | ||||
|             if (validationResult !== true) { | ||||
|                 throw new Error(validationResult); | ||||
|         objectPath.forEach(item => { | ||||
|             const key = this.openmct.objects.makeKeyString(item.identifier); | ||||
|             if (this.independentContexts.get(key)) { | ||||
|                 timeContext = this.independentContexts.get(key); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|             this.offsets = offsets; | ||||
|         return timeContext; | ||||
|     } | ||||
|  | ||||
|             const currentValue = this.activeClock.currentValue(); | ||||
|             const newBounds = { | ||||
|                 start: currentValue + offsets.start, | ||||
|                 end: currentValue + offsets.end | ||||
|             }; | ||||
| } | ||||
|  | ||||
|             this.bounds(newBounds); | ||||
|  | ||||
|             /** | ||||
|              * Event that is triggered when clock offsets change. | ||||
|              * @event clockOffsets | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {ClockOffsets} clockOffsets The newly activated clock | ||||
|              * offsets. | ||||
|              */ | ||||
|             this.emit("clockOffsets", offsets); | ||||
|         } | ||||
|  | ||||
|         return this.offsets; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Stop the currently active clock from ticking, and unset it. This will | ||||
|      * revert all views to showing a static time frame defined by the current | ||||
|      * bounds. | ||||
|      */ | ||||
|     TimeAPI.prototype.stopClock = function () { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return TimeAPI; | ||||
| }); | ||||
| export default TimeAPI; | ||||
|   | ||||
| @@ -19,241 +19,243 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import TimeAPI from "./TimeAPI"; | ||||
| import {createOpenMct} from "utils/testing"; | ||||
|  | ||||
| define(['./TimeAPI'], function (TimeAPI) { | ||||
|     describe("The Time API", function () { | ||||
|         let api; | ||||
|         let timeSystemKey; | ||||
|         let timeSystem; | ||||
|         let clockKey; | ||||
|         let clock; | ||||
|         let bounds; | ||||
|         let eventListener; | ||||
|         let toi; | ||||
| describe("The Time API", function () { | ||||
|     let api; | ||||
|     let timeSystemKey; | ||||
|     let timeSystem; | ||||
|     let clockKey; | ||||
|     let clock; | ||||
|     let bounds; | ||||
|     let eventListener; | ||||
|     let toi; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = createOpenMct(); | ||||
|         api = new TimeAPI(openmct); | ||||
|         timeSystemKey = "timeSystemKey"; | ||||
|         timeSystem = {key: timeSystemKey}; | ||||
|         clockKey = "someClockKey"; | ||||
|         clock = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         clock.currentValue.and.returnValue(100); | ||||
|         clock.key = clockKey; | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         eventListener = jasmine.createSpy("eventListener"); | ||||
|         toi = 111; | ||||
|     }); | ||||
|  | ||||
|     it("Supports setting and querying of time of interest", function () { | ||||
|         expect(api.timeOfInterest()).not.toBe(toi); | ||||
|         api.timeOfInterest(toi); | ||||
|         expect(api.timeOfInterest()).toBe(toi); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of valid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         expect(api.bounds()).not.toBe(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).not.toThrow(); | ||||
|         expect(api.bounds()).toEqual(bounds); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of invalid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 1, | ||||
|             end: 0 | ||||
|         }; | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|         bounds = {start: 1}; | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of previously registered time system with bounds", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystem, bounds); | ||||
|         }).not.toThrow(); | ||||
|         expect(api.timeSystem()).toBe(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of time system without bounds", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystemKey); | ||||
|         }).toThrow(); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("allows setting of timesystem without bounds with clock", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         api.addClock(clock); | ||||
|         api.clock(clockKey, { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystemKey); | ||||
|         }).not.toThrow(); | ||||
|         expect(api.timeSystem()).toBe(timeSystem); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when time system changes", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("timeSystem", eventListener); | ||||
|         api.timeSystem(timeSystemKey, bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when time of interest changes", function () { | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("timeOfInterest", eventListener); | ||||
|         api.timeOfInterest(toi); | ||||
|         expect(eventListener).toHaveBeenCalledWith(toi); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when bounds change", function () { | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("bounds", eventListener); | ||||
|         api.bounds(bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|     }); | ||||
|  | ||||
|     it("If bounds are set and TOI lies inside them, do not change TOI", function () { | ||||
|         api.timeOfInterest(6); | ||||
|         api.bounds({ | ||||
|             start: 1, | ||||
|             end: 10 | ||||
|         }); | ||||
|         expect(api.timeOfInterest()).toEqual(6); | ||||
|     }); | ||||
|  | ||||
|     it("If bounds are set and TOI lies outside them, reset TOI", function () { | ||||
|         api.timeOfInterest(11); | ||||
|         api.bounds({ | ||||
|             start: 1, | ||||
|             end: 10 | ||||
|         }); | ||||
|         expect(api.timeOfInterest()).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it("Maintains delta during tick", function () { | ||||
|     }); | ||||
|  | ||||
|     it("Allows registered time system to be activated", function () { | ||||
|     }); | ||||
|  | ||||
|     it("Allows a registered tick source to be activated", function () { | ||||
|         const mockTickSource = jasmine.createSpyObj("mockTickSource", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         mockTickSource.key = 'mockTickSource'; | ||||
|     }); | ||||
|  | ||||
|     describe(" when enabling a tick source", function () { | ||||
|         let mockTickSource; | ||||
|         let anotherMockTickSource; | ||||
|         const mockOffsets = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             api = new TimeAPI(); | ||||
|             timeSystemKey = "timeSystemKey"; | ||||
|             timeSystem = {key: timeSystemKey}; | ||||
|             clockKey = "someClockKey"; | ||||
|             clock = jasmine.createSpyObj("clock", [ | ||||
|             mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             clock.currentValue.and.returnValue(100); | ||||
|             clock.key = clockKey; | ||||
|             bounds = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|             eventListener = jasmine.createSpy("eventListener"); | ||||
|             toi = 111; | ||||
|         }); | ||||
|  | ||||
|         it("Supports setting and querying of time of interest", function () { | ||||
|             expect(api.timeOfInterest()).not.toBe(toi); | ||||
|             api.timeOfInterest(toi); | ||||
|             expect(api.timeOfInterest()).toBe(toi); | ||||
|         }); | ||||
|  | ||||
|         it("Allows setting of valid bounds", function () { | ||||
|             bounds = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|             expect(api.bounds()).not.toBe(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).not.toThrow(); | ||||
|             expect(api.bounds()).toEqual(bounds); | ||||
|         }); | ||||
|  | ||||
|         it("Disallows setting of invalid bounds", function () { | ||||
|             bounds = { | ||||
|                 start: 1, | ||||
|                 end: 0 | ||||
|             }; | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|             bounds = {start: 1}; | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|         }); | ||||
|  | ||||
|         it("Allows setting of previously registered time system with bounds", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystem, bounds); | ||||
|             }).not.toThrow(); | ||||
|             expect(api.timeSystem()).toBe(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("Disallows setting of time system without bounds", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystemKey); | ||||
|             }).toThrow(); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("allows setting of timesystem without bounds with clock", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             api.addClock(clock); | ||||
|             api.clock(clockKey, { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystemKey); | ||||
|             }).not.toThrow(); | ||||
|             expect(api.timeSystem()).toBe(timeSystem); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when time system changes", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("timeSystem", eventListener); | ||||
|             api.timeSystem(timeSystemKey, bounds); | ||||
|             expect(eventListener).toHaveBeenCalledWith(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when time of interest changes", function () { | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("timeOfInterest", eventListener); | ||||
|             api.timeOfInterest(toi); | ||||
|             expect(eventListener).toHaveBeenCalledWith(toi); | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when bounds change", function () { | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("bounds", eventListener); | ||||
|             api.bounds(bounds); | ||||
|             expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|         }); | ||||
|  | ||||
|         it("If bounds are set and TOI lies inside them, do not change TOI", function () { | ||||
|             api.timeOfInterest(6); | ||||
|             api.bounds({ | ||||
|                 start: 1, | ||||
|                 end: 10 | ||||
|             }); | ||||
|             expect(api.timeOfInterest()).toEqual(6); | ||||
|         }); | ||||
|  | ||||
|         it("If bounds are set and TOI lies outside them, reset TOI", function () { | ||||
|             api.timeOfInterest(11); | ||||
|             api.bounds({ | ||||
|                 start: 1, | ||||
|                 end: 10 | ||||
|             }); | ||||
|             expect(api.timeOfInterest()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         it("Maintains delta during tick", function () { | ||||
|         }); | ||||
|  | ||||
|         it("Allows registered time system to be activated", function () { | ||||
|         }); | ||||
|  | ||||
|         it("Allows a registered tick source to be activated", function () { | ||||
|             const mockTickSource = jasmine.createSpyObj("mockTickSource", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             mockTickSource.key = 'mockTickSource'; | ||||
|         }); | ||||
|  | ||||
|         describe(" when enabling a tick source", function () { | ||||
|             let mockTickSource; | ||||
|             let anotherMockTickSource; | ||||
|             const mockOffsets = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                     "on", | ||||
|                     "off", | ||||
|                     "currentValue" | ||||
|                 ]); | ||||
|                 mockTickSource.currentValue.and.returnValue(10); | ||||
|                 mockTickSource.key = "mts"; | ||||
|  | ||||
|                 anotherMockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                     "on", | ||||
|                     "off", | ||||
|                     "currentValue" | ||||
|                 ]); | ||||
|                 anotherMockTickSource.key = "amts"; | ||||
|                 anotherMockTickSource.currentValue.and.returnValue(10); | ||||
|  | ||||
|                 api.addClock(mockTickSource); | ||||
|                 api.addClock(anotherMockTickSource); | ||||
|             }); | ||||
|  | ||||
|             it("sets bounds based on current value", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(api.bounds()).toEqual({ | ||||
|                     start: 10, | ||||
|                     end: 11 | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("a new tick listener is registered", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("listener of existing tick source is reregistered", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 api.clock("amts", mockOffsets); | ||||
|                 expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("Allows the active clock to be set and unset", function () { | ||||
|                 expect(api.clock()).toBeUndefined(); | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(api.clock()).toBeDefined(); | ||||
|                 api.stopClock(); | ||||
|                 expect(api.clock()).toBeUndefined(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it("on tick, observes offsets, and indicates tick in bounds callback", function () { | ||||
|             const mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             mockTickSource.currentValue.and.returnValue(100); | ||||
|             let tickCallback; | ||||
|             const boundsCallback = jasmine.createSpy("boundsCallback"); | ||||
|             const clockOffsets = { | ||||
|                 start: -100, | ||||
|                 end: 100 | ||||
|             }; | ||||
|             mockTickSource.currentValue.and.returnValue(10); | ||||
|             mockTickSource.key = "mts"; | ||||
|  | ||||
|             anotherMockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             anotherMockTickSource.key = "amts"; | ||||
|             anotherMockTickSource.currentValue.and.returnValue(10); | ||||
|  | ||||
|             api.addClock(mockTickSource); | ||||
|             api.clock("mts", clockOffsets); | ||||
|  | ||||
|             api.on("bounds", boundsCallback); | ||||
|  | ||||
|             tickCallback = mockTickSource.on.calls.mostRecent().args[1]; | ||||
|             tickCallback(1000); | ||||
|             expect(boundsCallback).toHaveBeenCalledWith({ | ||||
|                 start: 900, | ||||
|                 end: 1100 | ||||
|             }, true); | ||||
|             api.addClock(anotherMockTickSource); | ||||
|         }); | ||||
|  | ||||
|         it("sets bounds based on current value", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(api.bounds()).toEqual({ | ||||
|                 start: 10, | ||||
|                 end: 11 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("a new tick listener is registered", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|         }); | ||||
|  | ||||
|         it("listener of existing tick source is reregistered", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             api.clock("amts", mockOffsets); | ||||
|             expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|         }); | ||||
|  | ||||
|         it("Allows the active clock to be set and unset", function () { | ||||
|             expect(api.clock()).toBeUndefined(); | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(api.clock()).toBeDefined(); | ||||
|             api.stopClock(); | ||||
|             expect(api.clock()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("on tick, observes offsets, and indicates tick in bounds callback", function () { | ||||
|         const mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         mockTickSource.currentValue.and.returnValue(100); | ||||
|         let tickCallback; | ||||
|         const boundsCallback = jasmine.createSpy("boundsCallback"); | ||||
|         const clockOffsets = { | ||||
|             start: -100, | ||||
|             end: 100 | ||||
|         }; | ||||
|         mockTickSource.key = "mts"; | ||||
|  | ||||
|         api.addClock(mockTickSource); | ||||
|         api.clock("mts", clockOffsets); | ||||
|  | ||||
|         api.on("bounds", boundsCallback); | ||||
|  | ||||
|         tickCallback = mockTickSource.on.calls.mostRecent().args[1]; | ||||
|         tickCallback(1000); | ||||
|         expect(boundsCallback).toHaveBeenCalledWith({ | ||||
|             start: 900, | ||||
|             end: 1100 | ||||
|         }, true); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										360
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| class TimeContext extends EventEmitter { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         //The Time System | ||||
|         this.timeSystems = new Map(); | ||||
|  | ||||
|         this.system = undefined; | ||||
|  | ||||
|         this.clocks = new Map(); | ||||
|  | ||||
|         this.boundsVal = { | ||||
|             start: undefined, | ||||
|             end: undefined | ||||
|         }; | ||||
|  | ||||
|         this.activeClock = undefined; | ||||
|         this.offsets = undefined; | ||||
|  | ||||
|         this.tick = this.tick.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the time system of the TimeAPI. | ||||
|      * @param {TimeSystem | string} timeSystem | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds | ||||
|      * @fires module:openmct.TimeAPI~timeSystem | ||||
|      * @returns {TimeSystem} The currently applied time system | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeSystem | ||||
|      */ | ||||
|     timeSystem(timeSystemOrKey, bounds) { | ||||
|         if (arguments.length >= 1) { | ||||
|             if (arguments.length === 1 && !this.activeClock) { | ||||
|                 throw new Error( | ||||
|                     "Must specify bounds when changing time system without " | ||||
|                     + "an active clock." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let timeSystem; | ||||
|  | ||||
|             if (timeSystemOrKey === undefined) { | ||||
|                 throw "Please provide a time system"; | ||||
|             } | ||||
|  | ||||
|             if (typeof timeSystemOrKey === 'string') { | ||||
|                 timeSystem = this.timeSystems.get(timeSystemOrKey); | ||||
|  | ||||
|                 if (timeSystem === undefined) { | ||||
|                     throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else if (typeof timeSystemOrKey === 'object') { | ||||
|                 timeSystem = timeSystemOrKey; | ||||
|  | ||||
|                 if (!this.timeSystems.has(timeSystem.key)) { | ||||
|                     throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else { | ||||
|                 throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; | ||||
|             } | ||||
|  | ||||
|             this.system = timeSystem; | ||||
|  | ||||
|             /** | ||||
|              * The time system used by the time | ||||
|              * conductor has changed. A change in Time System will always be | ||||
|              * followed by a bounds event specifying new query bounds. | ||||
|              * | ||||
|              * @event module:openmct.TimeAPI~timeSystem | ||||
|              * @property {TimeSystem} The value of the currently applied | ||||
|              * Time System | ||||
|              * */ | ||||
|             this.emit('timeSystem', this.system); | ||||
|             if (bounds) { | ||||
|                 this.bounds(bounds); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return this.system; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ValidationResult | ||||
|      * @property {boolean} valid Result of the validation - true or false. | ||||
|      * @property {string} message An error message if valid is false. | ||||
|      */ | ||||
|     /** | ||||
|      * Validate the given bounds. This can be used for pre-validation of bounds, | ||||
|      * for example by views validating user inputs. | ||||
|      * @param {TimeBounds} bounds The start and end time of the conductor. | ||||
|      * @returns {ValidationResult} A validation error, or true if valid | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      */ | ||||
|     validateBounds(bounds) { | ||||
|         if ((bounds.start === undefined) | ||||
|             || (bounds.end === undefined) | ||||
|             || isNaN(bounds.start) | ||||
|             || isNaN(bounds.end) | ||||
|         ) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Start and end must be specified as integer values" | ||||
|             }; | ||||
|         } else if (bounds.start > bounds.end) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Specified start date exceeds end bound" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             valid: true, | ||||
|             message: '' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     bounds(newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             const validationResult = this.validateBounds(newBounds); | ||||
|             if (validationResult.valid !== true) { | ||||
|                 throw new Error(validationResult.message); | ||||
|             } | ||||
|  | ||||
|             //Create a copy to avoid direct mutation of conductor bounds | ||||
|             this.boundsVal = JSON.parse(JSON.stringify(newBounds)); | ||||
|             /** | ||||
|              * The start time, end time, or both have been updated. | ||||
|              * @event bounds | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {TimeConductorBounds} bounds The newly updated bounds | ||||
|              * @property {boolean} [tick] `true` if the bounds update was due to | ||||
|              * a "tick" event (ie. was an automatic update), false otherwise. | ||||
|              */ | ||||
|             this.emit('bounds', this.boundsVal, false); | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validate the given offsets. This can be used for pre-validation of | ||||
|      * offsets, for example by views validating user inputs. | ||||
|      * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. | ||||
|      * @returns { ValidationResult } A validation error, and true/false if valid or not | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateOffsets | ||||
|      */ | ||||
|     validateOffsets(offsets) { | ||||
|         if ((offsets.start === undefined) | ||||
|             || (offsets.end === undefined) | ||||
|             || isNaN(offsets.start) | ||||
|             || isNaN(offsets.end) | ||||
|         ) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Start and end offsets must be specified as integer values" | ||||
|             }; | ||||
|         } else if (offsets.start >= offsets.end) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Specified start offset must be < end offset" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             valid: true, | ||||
|             message: '' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @typedef {Object} TimeBounds | ||||
|      * @property {number} start The start time displayed by the time conductor | ||||
|      * in ms since epoch. Epoch determined by currently active time system | ||||
|      * @property {number} end The end time displayed by the time conductor in ms | ||||
|      * since epoch. | ||||
|      * @memberof module:openmct.TimeAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ClockOffsets | ||||
|      * @property {number} start A time span relative to the current value of the | ||||
|      * ticking clock, from which start bounds will be calculated. This value must | ||||
|      * be < 0. When a clock is active, bounds will be calculated automatically | ||||
|      * based on the value provided by the clock, and the defined clock offsets. | ||||
|      * @property {number} end A time span relative to the current value of the | ||||
|      * ticking clock, from which end bounds will be calculated. This value must | ||||
|      * be >= 0. | ||||
|      */ | ||||
|     /** | ||||
|      * Get or set the currently applied clock offsets. If no parameter is provided, | ||||
|      * the current value will be returned. If provided, the new value will be | ||||
|      * used as the new clock offsets. | ||||
|      * @param {ClockOffsets} offsets | ||||
|      * @returns {ClockOffsets} | ||||
|      */ | ||||
|     clockOffsets(offsets) { | ||||
|         if (arguments.length > 0) { | ||||
|  | ||||
|             const validationResult = this.validateOffsets(offsets); | ||||
|             if (validationResult.valid !== true) { | ||||
|                 throw new Error(validationResult.message); | ||||
|             } | ||||
|  | ||||
|             this.offsets = offsets; | ||||
|  | ||||
|             const currentValue = this.activeClock.currentValue(); | ||||
|             const newBounds = { | ||||
|                 start: currentValue + offsets.start, | ||||
|                 end: currentValue + offsets.end | ||||
|             }; | ||||
|  | ||||
|             this.bounds(newBounds); | ||||
|  | ||||
|             /** | ||||
|              * Event that is triggered when clock offsets change. | ||||
|              * @event clockOffsets | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {ClockOffsets} clockOffsets The newly activated clock | ||||
|              * offsets. | ||||
|              */ | ||||
|             this.emit("clockOffsets", offsets); | ||||
|         } | ||||
|  | ||||
|         return this.offsets; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Stop the currently active clock from ticking, and unset it. This will | ||||
|      * revert all views to showing a static time frame defined by the current | ||||
|      * bounds. | ||||
|      */ | ||||
|     stopClock() { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} keyOrClock The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      */ | ||||
|     clock(keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @param {number} timestamp A time from which bounds will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     tick(timestamp) { | ||||
|         if (!this.activeClock) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const newBounds = { | ||||
|             start: timestamp + this.offsets.start, | ||||
|             end: timestamp + this.offsets.end | ||||
|         }; | ||||
|  | ||||
|         this.boundsVal = newBounds; | ||||
|         this.emit('bounds', this.boundsVal, true); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default TimeContext; | ||||
							
								
								
									
										155
									
								
								src/api/time/independentTimeAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/api/time/independentTimeAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 TimeAPI from "./TimeAPI"; | ||||
| import {createOpenMct} from "utils/testing"; | ||||
| describe("The Independent Time API", function () { | ||||
|     let api; | ||||
|     let domainObjectKey; | ||||
|     let clockKey; | ||||
|     let clock; | ||||
|     let bounds; | ||||
|     let independentBounds; | ||||
|     let eventListener; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = createOpenMct(); | ||||
|         api = new TimeAPI(openmct); | ||||
|         clockKey = "someClockKey"; | ||||
|         clock = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         clock.currentValue.and.returnValue(100); | ||||
|         clock.key = clockKey; | ||||
|         api.addClock(clock); | ||||
|         domainObjectKey = 'test-key'; | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         api.bounds(bounds); | ||||
|         independentBounds = { | ||||
|             start: 10, | ||||
|             end: 11 | ||||
|         }; | ||||
|         eventListener = jasmine.createSpy("eventListener"); | ||||
|     }); | ||||
|  | ||||
|     it("Creates an independent time context", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getIndependentContext(domainObjectKey); | ||||
|         expect(timeContext.bounds()).toEqual(independentBounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Gets an independent time context given the objectPath", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{ | ||||
|             identifier: { | ||||
|                 namespace: '', | ||||
|                 key: 'blah' | ||||
|             } | ||||
|         }, { identifier: domainObjectKey }]); | ||||
|         expect(timeContext.bounds()).toEqual(independentBounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("defaults to the global time context given the objectPath", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{ | ||||
|             identifier: { | ||||
|                 namespace: '', | ||||
|                 key: 'blah' | ||||
|             } | ||||
|         }]); | ||||
|         expect(timeContext.bounds()).toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of valid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         timeContext.bounds(bounds); | ||||
|         expect(timeContext.bounds()).toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of invalid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 1, | ||||
|             end: 0 | ||||
|         }; | ||||
|  | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(timeContext.bounds()).not.toBe(bounds); | ||||
|  | ||||
|         expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|         bounds = {start: 1}; | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when bounds change", function () { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         timeContext.on('bounds', eventListener); | ||||
|         timeContext.bounds(bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     describe(" when using real time clock", function () { | ||||
|         const mockOffsets = { | ||||
|             start: 10, | ||||
|             end: 11 | ||||
|         }; | ||||
|  | ||||
|         it("Emits an event when bounds change based on current value", function () { | ||||
|             let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|             let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             timeContext.clock('someClockKey', mockOffsets); | ||||
|             timeContext.on('bounds', eventListener); | ||||
|             timeContext.tick(10); | ||||
|             expect(eventListener).toHaveBeenCalledWith({ | ||||
|                 start: 20, | ||||
|                 end: 21 | ||||
|             }, true); | ||||
|             destroyTimeContext(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -78,6 +78,9 @@ class ImageExporter { | ||||
|         } | ||||
|  | ||||
|         return html2canvas(element, { | ||||
|             useCORS: true, | ||||
|             allowTaint: true, | ||||
|             logging: false, | ||||
|             onclone: function (document) { | ||||
|                 if (className) { | ||||
|                     const clonedElement = document.getElementById(exportId); | ||||
| @@ -87,7 +90,7 @@ class ImageExporter { | ||||
|                 element.id = oldId; | ||||
|             }, | ||||
|             removeContainer: true // Set to false to debug what html2canvas renders | ||||
|         }).then(function (canvas) { | ||||
|         }).then(canvas => { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             return new Promise(function (resolve, reject) { | ||||
| @@ -102,9 +105,10 @@ class ImageExporter { | ||||
|  | ||||
|                 return canvas.toBlob(blob => resolve({ blob }), mimeType); | ||||
|             }); | ||||
|         }, function (error) { | ||||
|             console.log('error capturing image', error); | ||||
|         }).catch(error => { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             console.error('error capturing image', error); | ||||
|             const errorDialog = overlays.dialog({ | ||||
|                 iconClass: 'error', | ||||
|                 message: 'Image was not captured successfully!', | ||||
|   | ||||
| @@ -41,7 +41,6 @@ const DEFAULTS = [ | ||||
|     'platform/forms', | ||||
|     'platform/identity', | ||||
|     'platform/persistence/aggregator', | ||||
|     'platform/persistence/queue', | ||||
|     'platform/policy', | ||||
|     'platform/entanglement', | ||||
|     'platform/search', | ||||
|   | ||||
| @@ -32,7 +32,7 @@ describe('the plugin', function () { | ||||
|     let openmct; | ||||
|     let composition; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|     beforeEach(() => { | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
| @@ -47,11 +47,6 @@ describe('the plugin', function () { | ||||
|             } | ||||
|         })); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         composition = openmct.composition.get({identifier}); | ||||
|  | ||||
|         spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ | ||||
|             { | ||||
|                 identifier: { | ||||
| @@ -66,6 +61,19 @@ describe('the plugin', function () { | ||||
|                 } | ||||
|             } | ||||
|         ])); | ||||
|  | ||||
|         spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => { | ||||
|             return Promise.resolve({ | ||||
|                 identifier: id | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         return new Promise((resolve) => { | ||||
|             openmct.once('start', resolve); | ||||
|             openmct.startHeadless(); | ||||
|         }).then(() => { | ||||
|             composition = openmct.composition.get({identifier}); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/plugins/DeviceClassifier/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/plugins/DeviceClassifier/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 Agent from "../../utils/agent/Agent"; | ||||
| import DeviceClassifier from "./src/DeviceClassifier"; | ||||
|  | ||||
| export default () => { | ||||
|     return (openmct) => { | ||||
|         openmct.on("start", () => { | ||||
|             const agent = new Agent(window); | ||||
|             DeviceClassifier(agent, window.document); | ||||
|         }); | ||||
|     }; | ||||
| }; | ||||
							
								
								
									
										72
									
								
								src/plugins/DeviceClassifier/src/DeviceClassifier.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/plugins/DeviceClassifier/src/DeviceClassifier.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /** | ||||
|  * Runs at application startup and adds a subset of the following | ||||
|  * CSS classes to the body of the document, depending on device | ||||
|  * attributes: | ||||
|  * | ||||
|  * * `mobile`: Phones or tablets. | ||||
|  * * `phone`: Phones specifically. | ||||
|  * * `tablet`: Tablets specifically. | ||||
|  * * `desktop`: Non-mobile devices. | ||||
|  * * `portrait`: Devices in a portrait-style orientation. | ||||
|  * * `landscape`: Devices in a landscape-style orientation. | ||||
|  * * `touch`: Device supports touch events. | ||||
|  * | ||||
|  * @param {utils/agent/Agent} agent | ||||
|  *        the service used to examine the user agent | ||||
|  * @param document the HTML DOM document object | ||||
|  * @constructor | ||||
|  */ | ||||
| import DeviceMatchers from "./DeviceMatchers"; | ||||
|  | ||||
| export default (agent, document) => { | ||||
|     const body = document.body; | ||||
|  | ||||
|     Object.keys(DeviceMatchers).forEach((key, index, array) => { | ||||
|         if (DeviceMatchers[key](agent)) { | ||||
|             body.classList.add(key); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     if (agent.isMobile()) { | ||||
|         const mediaQuery = window.matchMedia("(orientation: landscape)"); | ||||
|         function eventHandler(event) { | ||||
|             console.log("changed"); | ||||
|             if (event.matches) { | ||||
|                 body.classList.remove("portrait"); | ||||
|                 body.classList.add("landscape"); | ||||
|             } else { | ||||
|                 body.classList.remove("landscape"); | ||||
|                 body.classList.add("portrait"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (mediaQuery.addEventListener) { | ||||
|             mediaQuery.addEventListener(`change`, eventHandler); | ||||
|         } else { | ||||
|             // Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16 | ||||
|             mediaQuery.addListener(eventHandler); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										105
									
								
								src/plugins/DeviceClassifier/src/DeviceClassifierSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/plugins/DeviceClassifier/src/DeviceClassifierSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 DeviceClassifier from "./DeviceClassifier"; | ||||
| import DeviceMatchers from "./DeviceMatchers"; | ||||
|  | ||||
| const AGENT_METHODS = [ | ||||
|     "isMobile", | ||||
|     "isPhone", | ||||
|     "isTablet", | ||||
|     "isPortrait", | ||||
|     "isLandscape", | ||||
|     "isTouch" | ||||
| ]; | ||||
| const TEST_PERMUTATIONS = [ | ||||
|     ["isMobile", "isPhone", "isTouch", "isPortrait"], | ||||
|     ["isMobile", "isPhone", "isTouch", "isLandscape"], | ||||
|     ["isMobile", "isTablet", "isTouch", "isPortrait"], | ||||
|     ["isMobile", "isTablet", "isTouch", "isLandscape"], | ||||
|     ["isTouch"], | ||||
|     [] | ||||
| ]; | ||||
|  | ||||
| describe("DeviceClassifier", function () { | ||||
|     let mockAgent; | ||||
|     let mockDocument; | ||||
|     let mockClassList; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         mockAgent = jasmine.createSpyObj( | ||||
|             "agent", | ||||
|             AGENT_METHODS | ||||
|         ); | ||||
|  | ||||
|         mockClassList = jasmine.createSpyObj("classList", ["add"]); | ||||
|  | ||||
|         mockDocument = jasmine.createSpyObj( | ||||
|             "document", | ||||
|             {}, | ||||
|             { body: { classList: mockClassList } } | ||||
|         ); | ||||
|  | ||||
|         AGENT_METHODS.forEach(function (m) { | ||||
|             mockAgent[m].and.returnValue(false); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     TEST_PERMUTATIONS.forEach(function (trueMethods) { | ||||
|         const summary = | ||||
|       trueMethods.length === 0 | ||||
|           ? "device has no detected characteristics" | ||||
|           : "device " + trueMethods.join(", "); | ||||
|  | ||||
|         describe("when " + summary, function () { | ||||
|             beforeEach(function () { | ||||
|                 trueMethods.forEach(function (m) { | ||||
|                     mockAgent[m].and.returnValue(true); | ||||
|                 }); | ||||
|  | ||||
|                 // eslint-disable-next-line no-new | ||||
|                 DeviceClassifier(mockAgent, mockDocument); | ||||
|             }); | ||||
|  | ||||
|             it("adds classes for matching, detected characteristics", function () { | ||||
|                 Object.keys(DeviceMatchers) | ||||
|                     .filter(function (m) { | ||||
|                         return DeviceMatchers[m](mockAgent); | ||||
|                     }) | ||||
|                     .forEach(function (key) { | ||||
|                         expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key); | ||||
|                     }); | ||||
|             }); | ||||
|  | ||||
|             it("does not add classes for non-matching characteristics", function () { | ||||
|                 Object.keys(DeviceMatchers) | ||||
|                     .filter(function (m) { | ||||
|                         return !DeviceMatchers[m](mockAgent); | ||||
|                     }) | ||||
|                     .forEach(function (key) { | ||||
|                         expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith( | ||||
|                             key | ||||
|                         ); | ||||
|                     }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										57
									
								
								src/plugins/DeviceClassifier/src/DeviceMatchers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/plugins/DeviceClassifier/src/DeviceMatchers.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /** | ||||
|  * An object containing key-value pairs, where keys are symbolic of | ||||
|  * device attributes, and values are functions that take the | ||||
|  * `agent` as inputs and return boolean values indicating | ||||
|  * whether or not the current device has these attributes. | ||||
|  * | ||||
|  * For internal use by the mobile support bundle. | ||||
|  * | ||||
|  * @memberof src/plugins/DeviceClassifier | ||||
|  * @private | ||||
|  */ | ||||
|  | ||||
| export default { | ||||
|     mobile: function (agent) { | ||||
|         return agent.isMobile(); | ||||
|     }, | ||||
|     phone: function (agent) { | ||||
|         return agent.isPhone(); | ||||
|     }, | ||||
|     tablet: function (agent) { | ||||
|         return agent.isTablet(); | ||||
|     }, | ||||
|     desktop: function (agent) { | ||||
|         return !agent.isMobile(); | ||||
|     }, | ||||
|     portrait: function (agent) { | ||||
|         return agent.isPortrait(); | ||||
|     }, | ||||
|     landscape: function (agent) { | ||||
|         return agent.isLandscape(); | ||||
|     }, | ||||
|     touch: function (agent) { | ||||
|         return agent.isTouch(); | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										65
									
								
								src/plugins/DeviceClassifier/src/DeviceMatchersSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/plugins/DeviceClassifier/src/DeviceMatchersSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 DeviceMatchers from "./DeviceMatchers"; | ||||
|  | ||||
| describe("DeviceMatchers", function () { | ||||
|     let mockAgent; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         mockAgent = jasmine.createSpyObj("agent", [ | ||||
|             "isMobile", | ||||
|             "isPhone", | ||||
|             "isTablet", | ||||
|             "isPortrait", | ||||
|             "isLandscape", | ||||
|             "isTouch" | ||||
|         ]); | ||||
|     }); | ||||
|  | ||||
|     it("detects when a device is a desktop device", function () { | ||||
|         mockAgent.isMobile.and.returnValue(false); | ||||
|         expect(DeviceMatchers.desktop(mockAgent)).toBe(true); | ||||
|         mockAgent.isMobile.and.returnValue(true); | ||||
|         expect(DeviceMatchers.desktop(mockAgent)).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     function method(deviceType) { | ||||
|         return "is" + deviceType[0].toUpperCase() + deviceType.slice(1); | ||||
|     } | ||||
|  | ||||
|     [ | ||||
|         "mobile", | ||||
|         "phone", | ||||
|         "tablet", | ||||
|         "landscape", | ||||
|         "portrait", | ||||
|         "landscape", | ||||
|         "touch" | ||||
|     ].forEach(function (deviceType) { | ||||
|         it("detects when a device is a " + deviceType + " device", function () { | ||||
|             mockAgent[method(deviceType)].and.returnValue(true); | ||||
|             expect(DeviceMatchers[deviceType](mockAgent)).toBe(true); | ||||
|             mockAgent[method(deviceType)].and.returnValue(false); | ||||
|             expect(DeviceMatchers[deviceType](mockAgent)).toBe(false); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -96,11 +96,11 @@ export default { | ||||
|  | ||||
|         this.timestampKey = this.openmct.time.timeSystem().key; | ||||
|  | ||||
|         this.valueMetadata = this | ||||
|         this.valueMetadata = this.metadata ? this | ||||
|             .metadata | ||||
|             .valuesForHints(['range'])[0]; | ||||
|             .valuesForHints(['range'])[0] : undefined; | ||||
|  | ||||
|         this.valueKey = this.valueMetadata.key; | ||||
|         this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined; | ||||
|  | ||||
|         this.unsubscribe = this.openmct | ||||
|             .telemetry | ||||
| @@ -151,7 +151,10 @@ export default { | ||||
|                     size: 1, | ||||
|                     strategy: 'latest' | ||||
|                 }) | ||||
|                 .then((array) => this.updateValues(array[array.length - 1])); | ||||
|                 .then((array) => this.updateValues(array[array.length - 1])) | ||||
|                 .catch((error) => { | ||||
|                     console.warn('Error fetching data', error); | ||||
|                 }); | ||||
|         }, | ||||
|         updateBounds(bounds, isTick) { | ||||
|             this.bounds = bounds; | ||||
|   | ||||
| @@ -73,8 +73,9 @@ export default { | ||||
|         hasUnits() { | ||||
|             let itemsWithUnits = this.items.filter((item) => { | ||||
|                 let metadata = this.openmct.telemetry.getMetadata(item.domainObject); | ||||
|                 const valueMetadatas = metadata ? metadata.valueMetadatas : []; | ||||
|  | ||||
|                 return this.metadataHasUnits(metadata.valueMetadatas); | ||||
|                 return this.metadataHasUnits(valueMetadatas); | ||||
|  | ||||
|             }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										53
									
								
								src/plugins/charts/BarGraphCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/plugins/charts/BarGraphCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { BAR_GRAPH_KEY } from './BarGraphConstants'; | ||||
|  | ||||
| export default function BarGraphCompositionPolicy(openmct) { | ||||
|     function hasRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
|  | ||||
|         return rangeValues.length > 0; | ||||
|     } | ||||
|  | ||||
|     function hasBarGraphTelemetry(domainObject) { | ||||
|         if (!openmct.telemetry.isTelemetryObject(domainObject)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasRange(metadata); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         allow: function (parent, child) { | ||||
|             if ((parent.type === BAR_GRAPH_KEY) | ||||
|                 && (hasBarGraphTelemetry(child) === false) | ||||
|             ) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/plugins/charts/BarGraphConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/plugins/charts/BarGraphConstants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export const BAR_GRAPH_VIEW = 'bar-graph.view'; | ||||
| export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph'; | ||||
| export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector'; | ||||
							
								
								
									
										289
									
								
								src/plugins/charts/BarGraphPlot.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								src/plugins/charts/BarGraphPlot.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | ||||
| <template> | ||||
| <div ref="plotWrapper" | ||||
|      class="has-local-controls" | ||||
|      :class="{ 's-unsynced' : isZoomed }" | ||||
| > | ||||
|     <div v-if="isZoomed" | ||||
|          class="l-state-indicators" | ||||
|     > | ||||
|         <span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle" | ||||
|               title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data." | ||||
|         ></span> | ||||
|     </div> | ||||
|     <div ref="plot" | ||||
|          class="c-bar-chart" | ||||
|          @plotly_relayout="zoom" | ||||
|     ></div> | ||||
|     <div v-if="false" | ||||
|          ref="localControl" | ||||
|          class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover" | ||||
|     > | ||||
|         <button v-if="data.length" | ||||
|                 class="c-button icon-reset" | ||||
|                 :disabled="!isZoomed" | ||||
|                 title="Reset pan/zoom" | ||||
|                 @click="reset()" | ||||
|         > | ||||
|         </button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
| import Plotly from 'plotly-basic'; | ||||
|  | ||||
| const MULTI_AXES_X_PADDING_PERCENT = { | ||||
|     LEFT: 8, | ||||
|     RIGHT: 94 | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         data: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         plotAxisTitle: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isZoomed: false, | ||||
|             primaryYAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             }, | ||||
|             xAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         data: { | ||||
|             immediate: false, | ||||
|             handler: 'updateData' | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), { | ||||
|             responsive: true, | ||||
|             displayModeBar: false | ||||
|         }); | ||||
|         this.registerListeners(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.plotResizeObserver) { | ||||
|             this.plotResizeObserver.unobserve(this.$refs.plotWrapper); | ||||
|             clearTimeout(this.resizeTimer); | ||||
|         } | ||||
|  | ||||
|         if (this.removeBarColorListener) { | ||||
|             this.removeBarColorListener(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         getAxisMinMax(axis) { | ||||
|             const min = axis.autoSize | ||||
|                 ? '' | ||||
|                 : axis.min; | ||||
|             const max = axis.autoSize | ||||
|                 ? '' | ||||
|                 : axis.max; | ||||
|  | ||||
|             return { | ||||
|                 min, | ||||
|                 max | ||||
|             }; | ||||
|         }, | ||||
|         getLayout() { | ||||
|             const yAxesMeta = this.getYAxisMeta(); | ||||
|             const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']); | ||||
|             const xAxisDomain = this.getXAxisDomain(yAxesMeta); | ||||
|  | ||||
|             return { | ||||
|                 autosize: true, | ||||
|                 showlegend: false, | ||||
|                 textposition: 'auto', | ||||
|                 font: { | ||||
|                     family: 'Helvetica Neue, Helvetica, Arial, sans-serif', | ||||
|                     size: '12px', | ||||
|                     color: '#666' | ||||
|                 }, | ||||
|                 xaxis: { | ||||
|                     domain: xAxisDomain, | ||||
|                     range: [this.xAxisRange.min, this.xAxisRange.max], | ||||
|                     title: this.plotAxisTitle.xAxisTitle, | ||||
|                     automargin: true, | ||||
|                     fixedrange: true | ||||
|                 }, | ||||
|                 yaxis: primaryYaxis, | ||||
|                 margin: { | ||||
|                     l: 5, | ||||
|                     r: 5, | ||||
|                     t: 5, | ||||
|                     b: 0 | ||||
|                 }, | ||||
|                 paper_bgcolor: 'transparent', | ||||
|                 plot_bgcolor: 'transparent' | ||||
|             }; | ||||
|         }, | ||||
|         getYAxisMeta() { | ||||
|             const yAxisMeta = {}; | ||||
|  | ||||
|             this.data.forEach(datum => { | ||||
|                 const yAxisMetadata = datum.yAxisMetadata; | ||||
|                 const range = '1'; | ||||
|                 const side = 'left'; | ||||
|                 const name = ''; | ||||
|                 const unit = yAxisMetadata.units; | ||||
|  | ||||
|                 yAxisMeta[range] = { | ||||
|                     range, | ||||
|                     side, | ||||
|                     name, | ||||
|                     unit | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return yAxisMeta; | ||||
|         }, | ||||
|         getXAxisDomain(yAxisMeta) { | ||||
|             let leftPaddingPerc = 0; | ||||
|             let rightPaddingPerc = 100; | ||||
|             let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right')); | ||||
|             let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left')); | ||||
|             if (yAxisMeta && rightSide.length > 1) { | ||||
|                 rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT; | ||||
|             } | ||||
|  | ||||
|             if (yAxisMeta && leftSide.length > 1) { | ||||
|                 leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT; | ||||
|             } | ||||
|  | ||||
|             return [leftPaddingPerc / 100, rightPaddingPerc / 100]; | ||||
|         }, | ||||
|         getYaxisLayout(yAxisMeta) { | ||||
|             if (!yAxisMeta) { | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             const { name, range, side = 'left', unit } = yAxisMeta; | ||||
|             const title = `${name} ${unit ? '(' + unit + ')' : ''}`; | ||||
|             const yaxis = { | ||||
|                 automargin: true, | ||||
|                 fixedrange: true, | ||||
|                 title | ||||
|             }; | ||||
|  | ||||
|             let yAxistype = this.primaryYAxisRange; | ||||
|             if (range === '1') { | ||||
|                 yaxis.range = [yAxistype.min, yAxistype.max]; | ||||
|  | ||||
|                 return yaxis; | ||||
|             } | ||||
|  | ||||
|             yaxis.range = [yAxistype.min, yAxistype.max]; | ||||
|             yaxis.anchor = side.toLowerCase() === 'left' | ||||
|                 ? 'free' | ||||
|                 : 'x'; | ||||
|             yaxis.showline = side.toLowerCase() === 'left'; | ||||
|             yaxis.side = side.toLowerCase(); | ||||
|             yaxis.overlaying = 'y'; | ||||
|             yaxis.position = 0.01; | ||||
|  | ||||
|             return yaxis; | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.removeBarColorListener = this.openmct.objects.observe( | ||||
|                 this.domainObject, | ||||
|                 'configuration.barStyles', | ||||
|                 this.barColorChanged | ||||
|             ); | ||||
|             this.resizeTimer = false; | ||||
|             if (window.ResizeObserver) { | ||||
|                 this.plotResizeObserver = new ResizeObserver(() => { | ||||
|                     // debounce and trigger window resize so that plotly can resize the plot | ||||
|                     clearTimeout(this.resizeTimer); | ||||
|                     this.resizeTimer = setTimeout(() => { | ||||
|                         window.dispatchEvent(new Event('resize')); | ||||
|                     }, 250); | ||||
|                 }); | ||||
|                 this.plotResizeObserver.observe(this.$refs.plotWrapper); | ||||
|             } | ||||
|         }, | ||||
|         reset() { | ||||
|             this.updatePlot(); | ||||
|  | ||||
|             this.isZoomed = false; | ||||
|             this.$emit('subscribe'); | ||||
|         }, | ||||
|         barColorChanged() { | ||||
|             const colors = []; | ||||
|             const indices = []; | ||||
|             this.data.forEach((item, index) => { | ||||
|                 const key = item.key; | ||||
|                 const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color; | ||||
|                 indices.push(index); | ||||
|                 if (colorExists) { | ||||
|                     colors.push(this.domainObject.configuration.barStyles.series[key].color); | ||||
|                 } else { | ||||
|                     colors.push(item.marker.color); | ||||
|                 } | ||||
|             }); | ||||
|             const plotUpdate = { | ||||
|                 'marker.color': colors | ||||
|             }; | ||||
|             Plotly.restyle(this.$refs.plot, plotUpdate, indices); | ||||
|         }, | ||||
|         updateData() { | ||||
|             this.updatePlot(); | ||||
|         }, | ||||
|         updateLocalControlPosition() { | ||||
|             const localControl = this.$refs.localControl; | ||||
|             localControl.style.display = 'none'; | ||||
|  | ||||
|             const plot = this.$refs.plot; | ||||
|             const bgLayer = this.$el.querySelector('.bglayer'); | ||||
|  | ||||
|             const plotBoundingRect = plot.getBoundingClientRect(); | ||||
|             const bgLayerBoundingRect = bgLayer.getBoundingClientRect(); | ||||
|  | ||||
|             const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5; | ||||
|             const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5; | ||||
|  | ||||
|             localControl.style.top = `${top}px`; | ||||
|             localControl.style.left = `${left}px`; | ||||
|             localControl.style.display = 'block'; | ||||
|         }, | ||||
|         updatePlot() { | ||||
|             if (!this.$refs || !this.$refs.plot) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout()); | ||||
|         }, | ||||
|         zoom(eventData) { | ||||
|             const autorange = eventData['xaxis.autorange']; | ||||
|             const { autosize } = eventData; | ||||
|  | ||||
|             if (autosize || autorange) { | ||||
|                 this.isZoomed = false; | ||||
|                 this.reset(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.isZoomed = true; | ||||
|             this.$emit('unsubscribe'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										304
									
								
								src/plugins/charts/BarGraphView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								src/plugins/charts/BarGraphView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | ||||
| <!-- | ||||
|  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> | ||||
| <BarGraph ref="barGraph" | ||||
|           class="c-plot c-bar-chart-view" | ||||
|           :data="trace" | ||||
|           :plot-axis-title="plotAxisTitle" | ||||
|           @subscribe="subscribeToAll" | ||||
|           @unsubscribe="removeAllSubscriptions" | ||||
| /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import BarGraph from './BarGraphPlot.vue'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         BarGraph | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     data() { | ||||
|         this.telemetryObjects = {}; | ||||
|         this.telemetryObjectFormats = {}; | ||||
|         this.subscriptions = []; | ||||
|         this.composition = {}; | ||||
|  | ||||
|         return { | ||||
|             trace: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         plotAxisTitle() { | ||||
|             const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; | ||||
|             const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; | ||||
|             const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : ''; | ||||
|  | ||||
|             return { | ||||
|                 xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`, | ||||
|                 yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}` | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.loadComposition(); | ||||
|  | ||||
|         this.openmct.time.on('bounds', this.refreshData); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.time.off('bounds', this.refreshData); | ||||
|  | ||||
|         this.removeAllSubscriptions(); | ||||
|  | ||||
|         if (!this.composition) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.composition.off('add', this.addTelemetryObject); | ||||
|         this.composition.off('remove', this.removeTelemetryObject); | ||||
|     }, | ||||
|     methods: { | ||||
|         addTelemetryObject(telemetryObject) { | ||||
|             // grab information we need from the added telmetry object | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|             this.telemetryObjects[key] = telemetryObject; | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata); | ||||
|             const telemetryObjectPath = [telemetryObject, ...this.path]; | ||||
|             const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath); | ||||
|  | ||||
|             // make an update object that's a clone of the existing styles object so we preserve existing choices | ||||
|             let stylesUpdate = {}; | ||||
|             if (this.domainObject.configuration.barStyles.series[key]) { | ||||
|                 stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]); | ||||
|             } | ||||
|  | ||||
|             stylesUpdate.name = telemetryObject.name; | ||||
|             stylesUpdate.type = telemetryObject.type; | ||||
|             stylesUpdate.isAlias = telemetryIsAlias; | ||||
|  | ||||
|             // if something has changed, mutate and notify listeners | ||||
|             if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) { | ||||
|                 this.openmct.objects.mutate( | ||||
|                     this.domainObject, | ||||
|                     `configuration.barStyles.series["${key}"]`, | ||||
|                     stylesUpdate | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             // ask for the current telemetry data, then subcribe for changes | ||||
|             this.requestDataFor(telemetryObject); | ||||
|             this.subscribeToObject(telemetryObject); | ||||
|         }, | ||||
|         addTrace(trace, key) { | ||||
|             if (!this.trace.length) { | ||||
|                 this.trace = this.trace.concat([trace]); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let isInTrace = false; | ||||
|             const newTrace = this.trace.map((currentTrace, index) => { | ||||
|                 if (currentTrace.key !== key) { | ||||
|                     return currentTrace; | ||||
|                 } | ||||
|  | ||||
|                 isInTrace = true; | ||||
|  | ||||
|                 return trace; | ||||
|             }); | ||||
|  | ||||
|             this.trace = isInTrace ? newTrace : newTrace.concat([trace]); | ||||
|         }, | ||||
|         getAxisMetadata(telemetryObject) { | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             if (!metadata) { | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             const yAxisMetadata = metadata.valuesForHints(['range'])[0]; | ||||
|             //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only | ||||
|             const xAxisMetadata = metadata.valuesForHints(['range']); | ||||
|  | ||||
|             return { | ||||
|                 xAxisMetadata, | ||||
|                 yAxisMetadata | ||||
|             }; | ||||
|         }, | ||||
|         getOptions() { | ||||
|             const { start, end } = this.openmct.time.bounds(); | ||||
|  | ||||
|             return { | ||||
|                 end, | ||||
|                 start | ||||
|             }; | ||||
|         }, | ||||
|         loadComposition() { | ||||
|             this.composition = this.openmct.composition.get(this.domainObject); | ||||
|  | ||||
|             if (!this.composition) { | ||||
|                 this.addTelemetryObject(this.domainObject); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.composition.on('add', this.addTelemetryObject); | ||||
|             this.composition.on('remove', this.removeTelemetryObject); | ||||
|             this.composition.load(); | ||||
|         }, | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|                 telemetryObjects.forEach(this.requestDataFor); | ||||
|             } | ||||
|         }, | ||||
|         removeAllSubscriptions() { | ||||
|             this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|             this.subscriptions = []; | ||||
|         }, | ||||
|         removeSubscription(key) { | ||||
|             const found = this.subscriptions.findIndex(subscription => subscription.key === key); | ||||
|             if (found > -1) { | ||||
|                 this.subscriptions[found].unsubscribe(); | ||||
|                 this.subscriptions.splice(found, 1); | ||||
|             } | ||||
|         }, | ||||
|         removeTelemetryObject(identifier) { | ||||
|             const key = this.openmct.objects.makeKeyString(identifier); | ||||
|             delete this.telemetryObjects[key]; | ||||
|             if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) { | ||||
|                 delete this.telemetryObjectFormats[key]; | ||||
|             } | ||||
|  | ||||
|             if (this.domainObject.configuration.barStyles.series[key]) { | ||||
|                 delete this.domainObject.configuration.barStyles.series[key]; | ||||
|                 this.openmct.objects.mutate( | ||||
|                     this.domainObject, | ||||
|                     `configuration.barStyles.series["${key}"]`, | ||||
|                     undefined | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|  | ||||
|             this.trace = this.trace.filter(t => t.key !== key); | ||||
|         }, | ||||
|         addDataToGraph(telemetryObject, data, axisMetadata) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             if (data.message) { | ||||
|                 this.openmct.notifications.alert(data.message); | ||||
|             } | ||||
|  | ||||
|             if (!this.isDataInTimeRange(data, key)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let xValues = []; | ||||
|             let yValues = []; | ||||
|  | ||||
|             //populate X and Y values for plotly | ||||
|             axisMetadata.xAxisMetadata.forEach((metadata) => { | ||||
|                 xValues.push(metadata.name); | ||||
|                 if (data[metadata.key]) { | ||||
|                     const formattedValue = this.format(key, metadata.key, data); | ||||
|                     yValues.push(formattedValue); | ||||
|                 } else { | ||||
|                     yValues.push(null); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             const trace = { | ||||
|                 key, | ||||
|                 name: telemetryObject.name, | ||||
|                 x: xValues, | ||||
|                 y: yValues, | ||||
|                 text: yValues.map(String), | ||||
|                 xAxisMetadata: axisMetadata.xAxisMetadata, | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: 'bar', | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles.series[key].color | ||||
|                 }, | ||||
|                 hoverinfo: 'skip' | ||||
|             }; | ||||
|  | ||||
|             this.addTrace(trace, key); | ||||
|         }, | ||||
|         isDataInTimeRange(datum, key) { | ||||
|             const timeSystemKey = this.openmct.time.timeSystem().key; | ||||
|             let currentTimestamp = this.parse(key, timeSystemKey, datum); | ||||
|  | ||||
|             return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp; | ||||
|         }, | ||||
|         format(telemetryObjectKey, metadataKey, data) { | ||||
|             const formats = this.telemetryObjectFormats[telemetryObjectKey]; | ||||
|  | ||||
|             return formats[metadataKey].format(data); | ||||
|         }, | ||||
|         parse(telemetryObjectKey, metadataKey, datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const formats = this.telemetryObjectFormats[telemetryObjectKey]; | ||||
|  | ||||
|             return formats[metadataKey].parse(datum); | ||||
|         }, | ||||
|         requestDataFor(telemetryObject) { | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             this.openmct.telemetry.request(telemetryObject) | ||||
|                 .then(data => { | ||||
|                     data.forEach((datum) => { | ||||
|                         this.addDataToGraph(telemetryObject, datum, axisMetadata); | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     console.warn(`Error fetching data`, error); | ||||
|                 }); | ||||
|         }, | ||||
|         subscribeToObject(telemetryObject) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|  | ||||
|             const options = this.getOptions(); | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, | ||||
|                 data => this.addDataToGraph(telemetryObject, data, axisMetadata) | ||||
|                 , options); | ||||
|  | ||||
|             this.subscriptions.push({ | ||||
|                 key, | ||||
|                 unsubscribe | ||||
|             }); | ||||
|         }, | ||||
|         subscribeToAll() { | ||||
|             const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|             telemetryObjects.forEach(this.subscribeToObject); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										79
									
								
								src/plugins/charts/BarGraphViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/plugins/charts/BarGraphViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import BarGraphView from './BarGraphView.vue'; | ||||
| import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function BarGraphViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: BAR_GRAPH_VIEW, | ||||
|         name: 'Bar Graph', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|  | ||||
|         canEdit(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     let isCompact = isCompactView(objectPath); | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             BarGraphView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 options: { | ||||
|                                     compact: isCompact | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<bar-graph-view :options="options"></bar-graph-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; | ||||
| import Vue from 'vue'; | ||||
| import BarGraphOptions from "./BarGraphOptions.vue"; | ||||
|  | ||||
| export default function BarGraphInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         key: BAR_GRAPH_INSPECTOR_KEY, | ||||
|         name: 'Bar Graph Inspector View', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let object = selection[0][0].context.item; | ||||
|  | ||||
|             return object | ||||
|                 && object.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|         view: function (selection) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             BarGraphOptions | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject: selection[0][0].context.item | ||||
|                         }, | ||||
|                         template: '<bar-graph-options></bar-graph-options>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										70
									
								
								src/plugins/charts/inspector/BarGraphOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/plugins/charts/inspector/BarGraphOptions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, 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> | ||||
| <ul class="c-tree c-bar-graph-options"> | ||||
|     <h2 title="Display properties for this object">Bar Graph Series</h2> | ||||
|     <li v-for="series in domainObject.composition" | ||||
|         :key="series.key" | ||||
|     > | ||||
|         <series-options :item="series" | ||||
|                         :color-palette="colorPalette" | ||||
|         /> | ||||
|     </li> | ||||
| </ul> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import SeriesOptions from "./SeriesOptions.vue"; | ||||
| import ColorPalette from '@/ui/color/ColorPalette'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         SeriesOptions | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing(), | ||||
|             colorPalette: this.colorPalette | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canEdit() { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|     }, | ||||
|     methods: { | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										153
									
								
								src/plugins/charts/inspector/SeriesOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/plugins/charts/inspector/SeriesOptions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, 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> | ||||
| <ul> | ||||
|     <li class="c-tree__item menus-to-left" | ||||
|         :class="aliasCss" | ||||
|     > | ||||
|         <span class="c-disclosure-triangle is-enabled flex-elem" | ||||
|               :class="expandedCssClass" | ||||
|               @click="expanded = !expanded" | ||||
|         > | ||||
|         </span> | ||||
|  | ||||
|         <div class="c-object-label"> | ||||
|             <div :class="[seriesCss]"> | ||||
|             </div> | ||||
|             <div class="c-object-label__name">{{ name }}</div> | ||||
|         </div> | ||||
|     </li> | ||||
|     <ColorSwatch v-if="expanded" | ||||
|                  :current-color="currentColor" | ||||
|                  title="Manually set the color for this bar graph series." | ||||
|                  edit-title="Manually set the color for this bar graph series" | ||||
|                  view-title="The color for this bar graph series." | ||||
|                  short-label="Color" | ||||
|                  class="grid-properties" | ||||
|                  @colorSet="setColor" | ||||
|     /> | ||||
| </ul> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorSwatch from '@/ui/color/ColorSwatch.vue'; | ||||
| import Color from "@/ui/color/Color"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ColorSwatch | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         colorPalette: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentColor: undefined, | ||||
|             name: '', | ||||
|             type: '', | ||||
|             isAlias: false, | ||||
|             expanded: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         expandedCssClass() { | ||||
|             return this.expanded ? 'c-disclosure-triangle--expanded' : ''; | ||||
|         }, | ||||
|         seriesCss() { | ||||
|             const type = this.openmct.types.get(this.type); | ||||
|             if (type && type.definition && type.definition.cssClass) { | ||||
|                 return `c-object-label__type-icon ${type.definition.cssClass}`; | ||||
|             } | ||||
|  | ||||
|             return 'c-object-label__type-icon'; | ||||
|         }, | ||||
|         aliasCss() { | ||||
|             let cssClass = ''; | ||||
|             if (this.isAlias) { | ||||
|                 cssClass = 'is-alias'; | ||||
|             } | ||||
|  | ||||
|             return cssClass; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         item: { | ||||
|             handler() { | ||||
|                 this.initColorAndName(); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.key = this.openmct.objects.makeKeyString(this.item); | ||||
|         this.initColorAndName(); | ||||
|         this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.removeBarStylesListener) { | ||||
|             this.removeBarStylesListener(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         initColorAndName() { | ||||
|             // this is called before the plot is initialized | ||||
|             if (!this.domainObject.configuration.barStyles.series[this.key]) { | ||||
|                 const color = this.colorPalette.getNextColor().asHexString(); | ||||
|                 this.domainObject.configuration.barStyles.series[this.key] = { | ||||
|                     color, | ||||
|                     type: '', | ||||
|                     name: '', | ||||
|                     isAlias: false | ||||
|                 }; | ||||
|             } else if (!this.domainObject.configuration.barStyles.series[this.key].color) { | ||||
|                 this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString(); | ||||
|             } | ||||
|  | ||||
|             this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color; | ||||
|             this.name = this.domainObject.configuration.barStyles.series[this.key].name; | ||||
|             this.type = this.domainObject.configuration.barStyles.series[this.key].type; | ||||
|             this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias; | ||||
|  | ||||
|             let colorHexString = this.currentColor; | ||||
|             const colorObject = Color.fromHexString(colorHexString); | ||||
|  | ||||
|             this.colorPalette.remove(colorObject); | ||||
|         }, | ||||
|         setColor(chosenColor) { | ||||
|             this.currentColor = chosenColor.asHexString(); | ||||
|             this.openmct.objects.mutate( | ||||
|                 this.domainObject, | ||||
|                 `configuration.barStyles.series["${this.key}"].color`, | ||||
|                 this.currentColor | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										51
									
								
								src/plugins/charts/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/plugins/charts/plugin.js
									
									
									
									
									
										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. | ||||
|  *****************************************************************************/ | ||||
| import { BAR_GRAPH_KEY } from './BarGraphConstants'; | ||||
| import BarGraphViewProvider from './BarGraphViewProvider'; | ||||
| import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider'; | ||||
| import BarGraphCompositionPolicy from './BarGraphCompositionPolicy'; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.types.addType(BAR_GRAPH_KEY, { | ||||
|             key: BAR_GRAPH_KEY, | ||||
|             name: "Bar Graph", | ||||
|             cssClass: "icon-bar-chart", | ||||
|             description: "View data as a bar graph. Can be added to Display Layouts.", | ||||
|             creatable: true, | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = { | ||||
|                     barStyles: { series: {} } | ||||
|                 }; | ||||
|             }, | ||||
|             priority: 891 | ||||
|         }); | ||||
|  | ||||
|         openmct.objectViews.addProvider(new BarGraphViewProvider(openmct)); | ||||
|  | ||||
|         openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct)); | ||||
|  | ||||
|         openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow); | ||||
|     }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										486
									
								
								src/plugins/charts/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										486
									
								
								src/plugins/charts/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,486 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 {createOpenMct, resetApplicationState, createMouseEvent} from "utils/testing"; | ||||
| import Vue from "vue"; | ||||
| import BarGraphPlugin from "./plugin"; | ||||
| import BarGraph from './BarGraphPlot.vue'; | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY, BAR_GRAPH_INSPECTOR_KEY } from './BarGraphConstants'; | ||||
| import BarGraphOptions from "./inspector/BarGraphOptions.vue"; | ||||
|  | ||||
| describe("the plugin", function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let telemetryPromise; | ||||
|     let telemetryPromiseResolve; | ||||
|     let mockObjectPath; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'mock parent folder', | ||||
|                 type: 'time-strip', | ||||
|                 identifier: { | ||||
|                     key: 'mock-parent-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         const testTelemetry = [ | ||||
|             { | ||||
|                 'utc': 1, | ||||
|                 'some-key': 'some-value 1', | ||||
|                 'some-other-key': 'some-other-value 1' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 2, | ||||
|                 'some-key': 'some-value 2', | ||||
|                 'some-other-key': 'some-other-value 2' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 3, | ||||
|                 'some-key': 'some-value 3', | ||||
|                 'some-other-key': 'some-other-value 3' | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         telemetryPromise = new Promise((resolve) => { | ||||
|             telemetryPromiseResolve = resolve; | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|             telemetryPromiseResolve(testTelemetry); | ||||
|  | ||||
|             return telemetryPromise; | ||||
|         }); | ||||
|  | ||||
|         openmct.install(new BarGraphPlugin()); | ||||
|  | ||||
|         element = document.createElement("div"); | ||||
|         element.style.width = "640px"; | ||||
|         element.style.height = "480px"; | ||||
|         child = document.createElement("div"); | ||||
|         child.style.width = "640px"; | ||||
|         child.style.height = "480px"; | ||||
|         element.appendChild(child); | ||||
|         document.body.appendChild(element); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             unobserve() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         openmct.time.timeSystem("utc", { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
|         }); | ||||
|  | ||||
|         openmct.types.addType("test-object", { | ||||
|             creatable: true | ||||
|         }); | ||||
|  | ||||
|         openmct.on("start", done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach((done) => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         resetApplicationState(openmct).then(done).catch(done); | ||||
|     }); | ||||
|  | ||||
|     describe("The bar graph view", () => { | ||||
|         let testDomainObject; | ||||
|         let barGraphObject; | ||||
|         // eslint-disable-next-line no-unused-vars | ||||
|         let component; | ||||
|         let mockComposition; | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|             const getFunc = openmct.$injector.get; | ||||
|             spyOn(openmct.$injector, "get") | ||||
|                 .withArgs("exportImageService").and.returnValue({ | ||||
|                     exportPNG: () => {}, | ||||
|                     exportJPG: () => {} | ||||
|                 }) | ||||
|                 .and.callFake(getFunc); | ||||
|  | ||||
|             barGraphObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-plot" | ||||
|                 }, | ||||
|                 type: "telemetry.plot.bar-graph", | ||||
|                 name: "Test Bar Graph" | ||||
|             }; | ||||
|  | ||||
|             testDomainObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 configuration: { | ||||
|                     barStyles: { | ||||
|                         series: {} | ||||
|                     } | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testDomainObject); | ||||
|  | ||||
|                 return [testDomainObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             let viewContainer = document.createElement("div"); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     BarGraph | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     domainObject: barGraphObject, | ||||
|                     composition: openmct.composition.get(barGraphObject) | ||||
|                 }, | ||||
|                 template: "<BarGraph></BarGraph>" | ||||
|             }); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         it("provides a bar graph view", () => { | ||||
|             const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath); | ||||
|             const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW); | ||||
|             expect(plotViewProvider).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("Renders plotly bar graph", () => { | ||||
|             let barChartElement = element.querySelectorAll(".plotly"); | ||||
|             expect(barChartElement.length).toBe(1); | ||||
|         }); | ||||
|  | ||||
|         it("Handles dots in telemetry id", () => { | ||||
|             const dotFullTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "someNamespace", | ||||
|                     key: "~OpenMCT~outer.test-object.foo.bar" | ||||
|                 }, | ||||
|                 type: "test-dotful-object", | ||||
|                 name: "A Dotful Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key.foo.name.45", | ||||
|                         name: "Some dotful attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key.bar.344.rad", | ||||
|                         name: "Another dotful attribute", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath); | ||||
|             const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW); | ||||
|             const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]); | ||||
|             barGraphView.show(child, true); | ||||
|             expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object"); | ||||
|             mockComposition.emit('add', dotFullTelemetryObject); | ||||
|             expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object"); | ||||
|             barGraphView.destroy(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the bar graph objects", () => { | ||||
|         const mockObject = { | ||||
|             name: 'A very nice bar graph', | ||||
|             key: BAR_GRAPH_KEY, | ||||
|             creatable: true | ||||
|         }; | ||||
|  | ||||
|         it('defines a bar graph object type with the correct key', () => { | ||||
|             const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; | ||||
|             expect(objectDef.key).toEqual(mockObject.key); | ||||
|         }); | ||||
|  | ||||
|         it('is creatable', () => { | ||||
|             const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; | ||||
|             expect(objectDef.creatable).toEqual(mockObject.creatable); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The bar graph composition policy", () => { | ||||
|  | ||||
|         it("allows composition for telemetry that contain at least one range", () => { | ||||
|             const parent = { | ||||
|                 "composition": [], | ||||
|                 "configuration": {}, | ||||
|                 "name": "Some Bar Graph", | ||||
|                 "type": "telemetry.plot.bar-graph", | ||||
|                 "location": "mine", | ||||
|                 "modified": 1631005183584, | ||||
|                 "persisted": 1631005183502, | ||||
|                 "identifier": { | ||||
|                     "namespace": "", | ||||
|                     "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|                 } | ||||
|             }; | ||||
|             const testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|             const composition = openmct.composition.get(parent); | ||||
|             expect(() => { | ||||
|                 composition.add(testTelemetryObject); | ||||
|             }).not.toThrow(); | ||||
|             expect(parent.composition.length).toBe(1); | ||||
|         }); | ||||
|  | ||||
|         it("disallows composition for telemetry that don't contain any range hints", () => { | ||||
|             const parent = { | ||||
|                 "composition": [], | ||||
|                 "configuration": {}, | ||||
|                 "name": "Some Bar Graph", | ||||
|                 "type": "telemetry.plot.bar-graph", | ||||
|                 "location": "mine", | ||||
|                 "modified": 1631005183584, | ||||
|                 "persisted": 1631005183502, | ||||
|                 "identifier": { | ||||
|                     "namespace": "", | ||||
|                     "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|                 } | ||||
|             }; | ||||
|             const testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute" | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute" | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|             const composition = openmct.composition.get(parent); | ||||
|             expect(() => { | ||||
|                 composition.add(testTelemetryObject); | ||||
|             }).toThrow(); | ||||
|             expect(parent.composition.length).toBe(0); | ||||
|         }); | ||||
|     }); | ||||
|     describe('the inspector view', () => { | ||||
|         let mockComposition; | ||||
|         let testDomainObject; | ||||
|         let selection; | ||||
|         let plotInspectorView; | ||||
|         let viewContainer; | ||||
|         let optionsElement; | ||||
|         beforeEach(async () => { | ||||
|             testDomainObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             selection = [ | ||||
|                 [ | ||||
|                     { | ||||
|                         context: { | ||||
|                             item: { | ||||
|                                 id: "test-object", | ||||
|                                 identifier: { | ||||
|                                     key: "test-object", | ||||
|                                     namespace: '' | ||||
|                                 }, | ||||
|                                 type: "telemetry.plot.bar-graph", | ||||
|                                 configuration: { | ||||
|                                     barStyles: { | ||||
|                                         series: { | ||||
|                                             '~Some~foo.bar': { | ||||
|                                                 name: 'A telemetry object', | ||||
|                                                 type: 'some-type', | ||||
|                                                 isAlias: true | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 }, | ||||
|                                 composition: [ | ||||
|                                     { | ||||
|                                         key: '~Some~foo.bar' | ||||
|                                     } | ||||
|                                 ] | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         context: { | ||||
|                             item: { | ||||
|                                 type: 'time-strip', | ||||
|                                 identifier: { | ||||
|                                     key: 'some-other-key', | ||||
|                                     namespace: '' | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             ]; | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testDomainObject); | ||||
|  | ||||
|                 return [testDomainObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             viewContainer = document.createElement('div'); | ||||
|             child.append(viewContainer); | ||||
|  | ||||
|             const applicableViews = openmct.inspectorViews.get(selection); | ||||
|             plotInspectorView = applicableViews[0]; | ||||
|             plotInspectorView.show(viewContainer); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             optionsElement = element.querySelector('.c-bar-graph-options'); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             plotInspectorView.destroy(); | ||||
|         }); | ||||
|  | ||||
|         it('it renders the options', () => { | ||||
|             expect(optionsElement).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('shows the name', () => { | ||||
|             const seriesEl = optionsElement.querySelector('.c-object-label__name'); | ||||
|             expect(seriesEl.innerHTML).toEqual('A telemetry object'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										78
									
								
								src/plugins/clearData/ClearDataAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/plugins/clearData/ClearDataAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| function inSelectionPath(openmct, domainObject) { | ||||
|     const domainObjectIdentifier = domainObject.identifier; | ||||
|  | ||||
|     return openmct.selection.get().some(selectionPath => { | ||||
|         return selectionPath.some(objectInPath => { | ||||
|             const objectInPathIdentifier = objectInPath.context.item.identifier; | ||||
|  | ||||
|             return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export default class ClearDataAction { | ||||
|     constructor(openmct, appliesToObjects) { | ||||
|         this.name = 'Clear Data for Object'; | ||||
|         this.key = 'clear-data-action'; | ||||
|         this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; | ||||
|         this.cssClass = 'icon-clear-data'; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._appliesToObjects = appliesToObjects; | ||||
|     } | ||||
|     invoke(objectPath) { | ||||
|         let domainObject = null; | ||||
|         if (objectPath) { | ||||
|             domainObject = objectPath[0]; | ||||
|         } | ||||
|  | ||||
|         this._openmct.objectViews.emit('clearData', domainObject); | ||||
|     } | ||||
|     appliesTo(objectPath) { | ||||
|         if (!objectPath) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const contextualDomainObject = objectPath[0]; | ||||
|         // first check to see if this action applies to this sort of object at all | ||||
|         const appliesToThisObject = this._appliesToObjects.some(type => { | ||||
|             return contextualDomainObject.type === type; | ||||
|         }); | ||||
|         if (!appliesToThisObject) { | ||||
|             // we've selected something not applicable | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject); | ||||
|         if (objectInSelectionPath) { | ||||
|             return true; | ||||
|         } else { | ||||
|             // if this it doesn't match up, check to see if we're in a composition (i.e., layout) | ||||
|             const routerPath = this._openmct.router.path[0]; | ||||
|  | ||||
|             return routerPath.type === 'layout'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| define([ | ||||
|     './components/globalClearIndicator.vue', | ||||
|     './clearDataAction', | ||||
|     './ClearDataAction', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     GlobaClearIndicator, | ||||
|   | ||||
							
								
								
									
										140
									
								
								src/plugins/clearData/test/ClearDataActionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/plugins/clearData/test/ClearDataActionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ClearDataActionPlugin from '../plugin.js'; | ||||
| import ClearDataAction from '../ClearDataAction.js'; | ||||
|  | ||||
| describe('When the Clear Data Plugin is installed,', () => { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|     const goodMockSelectionPath = [[{ | ||||
|         context: { | ||||
|             item: { | ||||
|                 identifier: { | ||||
|                     key: 'apple', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }]]; | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         }, | ||||
|         selection: { | ||||
|             get: function () { | ||||
|                 return goodMockSelectionPath; | ||||
|             } | ||||
|         }, | ||||
|         objects: { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const mockObjectPath = [ | ||||
|         { | ||||
|             name: 'mockObject1', | ||||
|             type: 'apple' | ||||
|         }, | ||||
|         { | ||||
|             name: 'mockObject2', | ||||
|             type: 'banana' | ||||
|         } | ||||
|     ]; | ||||
|  | ||||
|     it('Global Clear Indicator is installed', () => { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockIndicatorProvider.add).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('Clear Data context menu action is installed', () => { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', () => { | ||||
|         const action = new ClearDataAction(openmct); | ||||
|  | ||||
|         action.invoke(mockObjectPath); | ||||
|  | ||||
|         expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]); | ||||
|     }); | ||||
|  | ||||
|     it('clears data on applicable objects', () => { | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('does not clear data on inapplicable objects', () => { | ||||
|         let action = new ClearDataAction(openmct, ['pineapple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('does not clear data if not in the selection path and not a layout', () => { | ||||
|         openmct.objects = { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|         openmct.router = { | ||||
|             path: [{type: 'not-a-layout'}] | ||||
|         }; | ||||
|  | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('does clear data if not in the selection path and is a layout', () => { | ||||
|         openmct.objects = { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|         openmct.router = { | ||||
|             path: [{type: 'layout'}] | ||||
|         }; | ||||
|  | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(true); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,64 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ClearDataActionPlugin from '../plugin.js'; | ||||
| import ClearDataAction from '../clearDataAction.js'; | ||||
|  | ||||
| describe('When the Clear Data Plugin is installed,', function () { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const mockObjectPath = [ | ||||
|         {name: 'mockObject1'}, | ||||
|         {name: 'mockObject2'} | ||||
|     ]; | ||||
|  | ||||
|     it('Global Clear Indicator is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockIndicatorProvider.add).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('Clear Data context menu action is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', function () { | ||||
|         let action = new ClearDataAction(openmct); | ||||
|  | ||||
|         action.invoke(mockObjectPath); | ||||
|  | ||||
|         expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]); | ||||
|     }); | ||||
| }); | ||||
| @@ -19,40 +19,41 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| define(function () { | ||||
| 
 | ||||
|     /** | ||||
|      * An object containing key-value pairs, where keys are symbolic of | ||||
|      * device attributes, and values are functions that take the | ||||
|      * `agentService` as inputs and return boolean values indicating | ||||
|      * whether or not the current device has these attributes. | ||||
|      * | ||||
|      * For internal use by the mobile support bundle. | ||||
|      * | ||||
|      * @memberof platform/commonUI/mobile | ||||
|      * @private | ||||
|      */ | ||||
| import Clock from './components/Clock.vue'; | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default function ClockViewProvider(openmct) { | ||||
|     return { | ||||
|         mobile: function (agentService) { | ||||
|             return agentService.isMobile(); | ||||
|         key: 'clock.view', | ||||
|         name: 'Clock', | ||||
|         cssClass: 'icon-clock', | ||||
|         canView(domainObject) { | ||||
|             return domainObject.type === 'clock'; | ||||
|         }, | ||||
|         phone: function (agentService) { | ||||
|             return agentService.isPhone(); | ||||
|         }, | ||||
|         tablet: function (agentService) { | ||||
|             return agentService.isTablet(); | ||||
|         }, | ||||
|         desktop: function (agentService) { | ||||
|             return !agentService.isMobile(); | ||||
|         }, | ||||
|         portrait: function (agentService) { | ||||
|             return agentService.isPortrait(); | ||||
|         }, | ||||
|         landscape: function (agentService) { | ||||
|             return agentService.isLandscape(); | ||||
|         }, | ||||
|         touch: function (agentService) { | ||||
|             return agentService.isTouch(); | ||||
| 
 | ||||
|         view: function (domainObject) { | ||||
|             let component; | ||||
| 
 | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             Clock | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                         }, | ||||
|                         template: '<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> | ||||
							
								
								
									
										64
									
								
								src/plugins/clock/components/ClockIndicator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/clock/components/ClockIndicator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| <!-- | ||||
|  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="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 === true) { | ||||
|             const clockIndicator = new Vue ({ | ||||
|                 components: { | ||||
|                     ClockIndicator | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct | ||||
|                 }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         indicatorFormat: CLOCK_INDICATOR_FORMAT | ||||
|                     }; | ||||
|                 }, | ||||
|                 template: '<ClockIndicator :indicator-format="indicatorFormat" />' | ||||
|             }); | ||||
|             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; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										231
									
								
								src/plugins/clock/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/plugins/clock/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import clockPlugin from './plugin'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| describe("Clock plugin:", () => { | ||||
|     let openmct; | ||||
|     let clockDefinition; | ||||
|     let element; | ||||
|     let child; | ||||
|     let appHolder; | ||||
|  | ||||
|     let clockDomainObject; | ||||
|  | ||||
|     function setupClock(enableClockIndicator) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             clockDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'clock', | ||||
|                     namespace: 'test-namespace' | ||||
|                 }, | ||||
|                 type: 'clock' | ||||
|             }; | ||||
|  | ||||
|             appHolder = document.createElement('div'); | ||||
|             appHolder.style.width = '640px'; | ||||
|             appHolder.style.height = '480px'; | ||||
|             document.body.appendChild(appHolder); | ||||
|  | ||||
|             openmct = createOpenMct(); | ||||
|  | ||||
|             element = document.createElement('div'); | ||||
|             child = document.createElement('div'); | ||||
|             element.appendChild(child); | ||||
|  | ||||
|             openmct.install(clockPlugin({ enableClockIndicator })); | ||||
|  | ||||
|             clockDefinition = openmct.types.get('clock').definition; | ||||
|             clockDefinition.initialize(clockDomainObject); | ||||
|  | ||||
|             openmct.on('start', resolve); | ||||
|             openmct.start(appHolder); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     describe("Clock view:", () => { | ||||
|         let clockViewProvider; | ||||
|         let clockView; | ||||
|         let clockViewObject; | ||||
|         let mutableClockObject; | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockViewObject = { | ||||
|                 ...clockDomainObject, | ||||
|                 id: "test-object", | ||||
|                 name: 'Clock', | ||||
|                 configuration: { | ||||
|                     baseFormat: 'YYYY/MM/DD hh:mm:ss', | ||||
|                     use24: 'clock12', | ||||
|                     timezone: 'UTC' | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject)); | ||||
|             spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]); | ||||
|             clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view'); | ||||
|  | ||||
|             mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier); | ||||
|  | ||||
|             clockView = clockViewProvider.view(mutableClockObject); | ||||
|             clockView.show(child); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             clockView.destroy(); | ||||
|             openmct.objects.destroyMutable(mutableClockObject); | ||||
|             if (appHolder) { | ||||
|                 appHolder.remove(); | ||||
|             } | ||||
|  | ||||
|             return resetApplicationState(openmct); | ||||
|         }); | ||||
|  | ||||
|         it("has name as Clock", () => { | ||||
|             expect(clockDefinition.name).toEqual('Clock'); | ||||
|         }); | ||||
|  | ||||
|         it("is creatable", () => { | ||||
|             expect(clockDefinition.creatable).toEqual(true); | ||||
|         }); | ||||
|  | ||||
|         it("provides clock view", () => { | ||||
|             expect(clockViewProvider).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("renders clock element", () => { | ||||
|             const clockElement = element.querySelectorAll('.c-clock'); | ||||
|             expect(clockElement.length).toBe(1); | ||||
|         }); | ||||
|  | ||||
|         it("renders major elements", () => { | ||||
|             const clockElement = element.querySelector('.c-clock'); | ||||
|             const timezone = clockElement.querySelector('.c-clock__timezone'); | ||||
|             const time = clockElement.querySelector('.c-clock__value'); | ||||
|             const amPm = clockElement.querySelector('.c-clock__ampm'); | ||||
|             const hasMajorElements = Boolean(timezone && time && amPm); | ||||
|  | ||||
|             expect(hasMajorElements).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("renders time in UTC", () => { | ||||
|             const clockElement = element.querySelector('.c-clock'); | ||||
|             const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim(); | ||||
|  | ||||
|             expect(timezone).toBe('UTC'); | ||||
|         }); | ||||
|  | ||||
|         it("updates the 24 hour option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.use24).toBe('clock12'); | ||||
|             const new24Option = 'clock24'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.use24).toBe(new24Option); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option); | ||||
|         }); | ||||
|  | ||||
|         it("updates the timezone option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.timezone).toBe('UTC'); | ||||
|             const newZone = 'CST6CDT'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.timezone).toBe(newZone); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone); | ||||
|         }); | ||||
|  | ||||
|         it("updates the time format option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss'); | ||||
|             const newFormat = 'hh:mm:ss'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.baseFormat).toBe(newFormat); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Clock Indicator view:", () => { | ||||
|         let clockIndicator; | ||||
|  | ||||
|         afterEach(() => { | ||||
|             if (clockIndicator) { | ||||
|                 clockIndicator.remove(); | ||||
|             } | ||||
|  | ||||
|             clockIndicator = undefined; | ||||
|             if (appHolder) { | ||||
|                 appHolder.remove(); | ||||
|             } | ||||
|  | ||||
|             return resetApplicationState(openmct); | ||||
|         }); | ||||
|  | ||||
|         it("doesn't exist", async () => { | ||||
|             await setupClock(false); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator'); | ||||
|  | ||||
|             const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined; | ||||
|             expect(clockIndicatorMissing).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("exists", async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator').element; | ||||
|  | ||||
|             const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined; | ||||
|             expect(hasClockIndicator).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("contains text", async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator').element; | ||||
|  | ||||
|             const clockIndicatorText = clockIndicator.textContent.trim(); | ||||
|             const textIncludesUTC = clockIndicatorText.includes('UTC'); | ||||
|  | ||||
|             expect(textIncludesUTC).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -65,7 +65,7 @@ export default class Condition extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         this.trigger = conditionConfiguration.configuration.trigger; | ||||
|         this.description = ''; | ||||
|         this.summary = ''; | ||||
|     } | ||||
|  | ||||
|     updateResult(datum) { | ||||
| @@ -134,7 +134,6 @@ export default class Condition extends EventEmitter { | ||||
|         criterionConfigurations.forEach((criterionConfiguration) => { | ||||
|             this.addCriterion(criterionConfiguration); | ||||
|         }); | ||||
|         this.updateDescription(); | ||||
|     } | ||||
|  | ||||
|     updateCriteria(criterionConfigurations) { | ||||
| @@ -146,7 +145,6 @@ export default class Condition extends EventEmitter { | ||||
|         this.criteria.forEach((criterion) => { | ||||
|             criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); | ||||
|         }); | ||||
|         this.updateDescription(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -200,7 +198,6 @@ export default class Condition extends EventEmitter { | ||||
|             criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); | ||||
|             criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj)); | ||||
|             this.criteria.splice(found.index, 1, newCriterion); | ||||
|             this.updateDescription(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -216,7 +213,6 @@ export default class Condition extends EventEmitter { | ||||
|             }); | ||||
|             criterion.destroy(); | ||||
|             this.criteria.splice(found.index, 1); | ||||
|             this.updateDescription(); | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
| @@ -228,7 +224,6 @@ export default class Condition extends EventEmitter { | ||||
|         let found = this.findCriterion(criterion.id); | ||||
|         if (found) { | ||||
|             this.criteria[found.index] = criterion.data; | ||||
|             this.updateDescription(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -254,8 +249,7 @@ export default class Condition extends EventEmitter { | ||||
|  | ||||
|             description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`; | ||||
|         }); | ||||
|         this.description = description; | ||||
|         this.conditionManager.updateConditionDescription(this); | ||||
|         this.summary = description; | ||||
|     } | ||||
|  | ||||
|     getTriggerDescription() { | ||||
|   | ||||
| @@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     updateConditionTelemetryObjects() { | ||||
|         this.conditions.forEach((condition) => condition.updateTelemetryObjects()); | ||||
|         this.conditions.forEach((condition) => { | ||||
|             condition.updateTelemetryObjects(); | ||||
|             let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id); | ||||
|             if (index > -1) { | ||||
|                 //Only assign the summary, don't mutate the domain object | ||||
|                 this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     removeConditionTelemetryObjects() { | ||||
| @@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateConditionDescription(condition) { | ||||
|         condition.updateDescription(); | ||||
|  | ||||
|         return condition.summary; | ||||
|     } | ||||
|  | ||||
|     updateCondition(conditionConfiguration) { | ||||
|         let condition = this.findConditionById(conditionConfiguration.id); | ||||
|         if (condition) { | ||||
|             condition.update(conditionConfiguration); | ||||
|             conditionConfiguration.summary = this.updateConditionDescription(condition); | ||||
|         } | ||||
|  | ||||
|         let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); | ||||
| @@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateConditionDescription(condition) { | ||||
|         const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id)); | ||||
|         if (found.summary !== condition.description) { | ||||
|             found.summary = condition.description; | ||||
|             this.persistConditions(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     initCondition(conditionConfiguration, index) { | ||||
|         let condition = new Condition(conditionConfiguration, this.openmct, this); | ||||
|         conditionConfiguration.summary = this.updateConditionDescription(condition); | ||||
|  | ||||
|         if (index !== undefined) { | ||||
|             this.conditions.splice(index + 1, 0, condition); | ||||
|         } else { | ||||
|   | ||||
| @@ -33,8 +33,10 @@ export default class ConditionSetViewProvider { | ||||
|         this.cssClass = 'icon-conditional'; | ||||
|     } | ||||
|  | ||||
|     canView(domainObject) { | ||||
|         return domainObject.type === 'conditionSet'; | ||||
|     canView(domainObject, objectPath) { | ||||
|         const isConditionSet = domainObject.type === 'conditionSet'; | ||||
|  | ||||
|         return isConditionSet && this.openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     canEdit(domainObject) { | ||||
|   | ||||
| @@ -244,7 +244,7 @@ export default { | ||||
|                 this.telemetryMetadataOptions = []; | ||||
|                 telemetryObjects.forEach(telemetryObject => { | ||||
|                     let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|                     this.addMetaDataOptions(telemetryMetadata.values()); | ||||
|                     this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []); | ||||
|                 }); | ||||
|                 this.updateOperations(); | ||||
|             } | ||||
|   | ||||
| @@ -192,7 +192,11 @@ export default { | ||||
|             this.telemetry.forEach((telemetryObject) => { | ||||
|                 const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|                 let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|                 this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice(); | ||||
|                 if (telemetryMetadata) { | ||||
|                     this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice(); | ||||
|                 } else { | ||||
|                     this.telemetryMetadataOptions[id] = []; | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         addTestInput(testInput) { | ||||
|   | ||||
| @@ -273,10 +273,7 @@ export default { | ||||
|             this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( | ||||
|                 (objectPath) => { | ||||
|                     this.objectPath = objectPath; | ||||
|                     this.navigateToPath = '#/browse/' + this.objectPath | ||||
|                         .map(o => o && this.openmct.objects.makeKeyString(o.identifier)) | ||||
|                         .reverse() | ||||
|                         .join('/'); | ||||
|                     this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); | ||||
|                 } | ||||
|             ); | ||||
|         }, | ||||
|   | ||||
| @@ -141,6 +141,7 @@ const NON_STYLEABLE_CONTAINER_TYPES = [ | ||||
| const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [ | ||||
|     'line-view', | ||||
|     'box-view', | ||||
|     'ellipse-view', | ||||
|     'image-view' | ||||
| ]; | ||||
|  | ||||
| @@ -296,10 +297,7 @@ export default { | ||||
|             this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( | ||||
|                 (objectPath) => { | ||||
|                     this.objectPath = objectPath; | ||||
|                     this.navigateToPath = '#/browse/' + this.objectPath | ||||
|                         .map(o => o && this.openmct.objects.makeKeyString(o.identifier)) | ||||
|                         .reverse() | ||||
|                         .join('/'); | ||||
|                     this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); | ||||
|                 } | ||||
|             ); | ||||
|         }, | ||||
| @@ -321,7 +319,7 @@ export default { | ||||
|             if (item) { | ||||
|                 const type = this.openmct.types.get(item.type); | ||||
|                 if (type && type.definition) { | ||||
|                     creatable = (type.definition.creatable === true); | ||||
|                     creatable = (type.definition.creatable !== undefined && (type.definition.creatable === 'true' || type.definition.creatable === true)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { | ||||
|                 const timeSystem = this.openmct.time.timeSystem(); | ||||
|  | ||||
|                 telemetryRequestsResults.forEach((results, index) => { | ||||
|                     const latestDatum = results.length ? results[results.length - 1] : {}; | ||||
|                     const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {}; | ||||
|                     const datumId = keys[index]; | ||||
|                     const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); | ||||
|  | ||||
|   | ||||
| @@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter { | ||||
|                 id: this.id, | ||||
|                 data: this.formatData(normalizedDatum) | ||||
|             }; | ||||
|         }).catch((error) => { | ||||
|             return { | ||||
|                 id: this.id, | ||||
|                 data: this.formatData() | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue"; | ||||
| import Vue from 'vue'; | ||||
| import {getApplicableStylesForItem} from "./utils/styleUtils"; | ||||
| import ConditionManager from "@/plugins/condition/ConditionManager"; | ||||
| import StyleRuleManager from "./StyleRuleManager"; | ||||
|  | ||||
| describe('the plugin', function () { | ||||
|     let conditionSetDefinition; | ||||
| @@ -96,8 +97,12 @@ describe('the plugin', function () { | ||||
|  | ||||
|         mockListener = jasmine.createSpy('mockListener'); | ||||
|  | ||||
|         openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); | ||||
|  | ||||
|         conditionSetDefinition.initialize(mockConditionSetDomainObject); | ||||
|  | ||||
|         spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
| @@ -126,21 +131,6 @@ describe('the plugin', function () { | ||||
|             expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue(); | ||||
|             expect(mockConditionSetDomainObject.composition.length).toEqual(0); | ||||
|         }); | ||||
|  | ||||
|         it('provides a view', () => { | ||||
|             const testViewObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "conditionSet", | ||||
|                 configuration: { | ||||
|                     conditionCollection: [] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|             let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view'); | ||||
|             expect(conditionSetView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     describe('the condition set usage for multiple display layout items', () => { | ||||
| @@ -230,7 +220,7 @@ describe('the plugin', function () { | ||||
|         }; | ||||
|         const staticStyle = { | ||||
|             "style": { | ||||
|                 "backgroundColor": "#717171", | ||||
|                 "backgroundColor": "#666666", | ||||
|                 "border": "1px solid #00ffff" | ||||
|             } | ||||
|         }; | ||||
| @@ -238,7 +228,7 @@ describe('the plugin', function () { | ||||
|             "conditionId": "39584410-cbf9-499e-96dc-76f27e69885d", | ||||
|             "style": { | ||||
|                 "isStyleInvisible": "", | ||||
|                 "backgroundColor": "#717171", | ||||
|                 "backgroundColor": "#666666", | ||||
|                 "border": "1px solid #ffff00" | ||||
|             } | ||||
|         }; | ||||
| @@ -250,7 +240,7 @@ describe('the plugin', function () { | ||||
|                 "configuration": { | ||||
|                     "items": [ | ||||
|                         { | ||||
|                             "fill": "#717171", | ||||
|                             "fill": "#666666", | ||||
|                             "stroke": "", | ||||
|                             "x": 1, | ||||
|                             "y": 1, | ||||
| @@ -259,12 +249,22 @@ describe('the plugin', function () { | ||||
|                             "type": "box-view", | ||||
|                             "id": "89b88746-d325-487b-aec4-11b79afff9e8" | ||||
|                         }, | ||||
|                         { | ||||
|                             "fill": "#666666", | ||||
|                             "stroke": "", | ||||
|                             "x": 1, | ||||
|                             "y": 1, | ||||
|                             "width": 10, | ||||
|                             "height": 5, | ||||
|                             "type": "ellipse-view", | ||||
|                             "id": "19b88746-d325-487b-aec4-11b79afff9z8" | ||||
|                         }, | ||||
|                         { | ||||
|                             "x": 18, | ||||
|                             "y": 9, | ||||
|                             "x2": 23, | ||||
|                             "y2": 4, | ||||
|                             "stroke": "#717171", | ||||
|                             "stroke": "#666666", | ||||
|                             "type": "line-view", | ||||
|                             "id": "57d49a28-7863-43bd-9593-6570758916f0" | ||||
|                         }, | ||||
| @@ -299,12 +299,12 @@ describe('the plugin', function () { | ||||
|                 "y": 9, | ||||
|                 "x2": 23, | ||||
|                 "y2": 4, | ||||
|                 "stroke": "#717171", | ||||
|                 "stroke": "#666666", | ||||
|                 "type": "line-view", | ||||
|                 "id": "57d49a28-7863-43bd-9593-6570758916f0" | ||||
|             }; | ||||
|             boxLayoutItem = { | ||||
|                 "fill": "#717171", | ||||
|                 "fill": "#666666", | ||||
|                 "stroke": "", | ||||
|                 "x": 1, | ||||
|                 "y": 1, | ||||
| @@ -712,4 +712,123 @@ describe('the plugin', function () { | ||||
|             expect(result[2]).toBeUndefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('canView of ConditionSetViewProvider', () => { | ||||
|         let conditionSetView; | ||||
|         const testViewObject = { | ||||
|             id: "test-object", | ||||
|             type: "conditionSet", | ||||
|             configuration: { | ||||
|                 conditionCollection: [] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|             conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view'); | ||||
|         }); | ||||
|  | ||||
|         it('provides a view', () => { | ||||
|             expect(conditionSetView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('returns true for type `conditionSet` and is a navigated Object', () => { | ||||
|             openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); | ||||
|  | ||||
|             const isCanView = conditionSetView.canView(testViewObject, []); | ||||
|  | ||||
|             expect(isCanView).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it('returns false for type `conditionSet` and is not a navigated Object', () => { | ||||
|             openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false); | ||||
|  | ||||
|             const isCanView = conditionSetView.canView(testViewObject, []); | ||||
|  | ||||
|             expect(isCanView).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         it('returns false for type `notConditionSet` and is a navigated Object', () => { | ||||
|             openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); | ||||
|             testViewObject.type = 'notConditionSet'; | ||||
|             const isCanView = conditionSetView.canView(testViewObject, []); | ||||
|  | ||||
|             expect(isCanView).toBe(false); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('The Style Rule Manager', () => { | ||||
|         it('should subscribe to the conditionSet after the editor saves', async () => { | ||||
|             const stylesObject = { | ||||
|                 "styles": [ | ||||
|                     { | ||||
|                         "conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd", | ||||
|                         "style": { | ||||
|                             "backgroundColor": "#38761d", | ||||
|                             "border": "", | ||||
|                             "color": "#073763", | ||||
|                             "isStyleInvisible": "" | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", | ||||
|                         "style": { | ||||
|                             "backgroundColor": "#980000", | ||||
|                             "border": "", | ||||
|                             "color": "#ff9900", | ||||
|                             "isStyleInvisible": "" | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 "staticStyle": { | ||||
|                     "style": { | ||||
|                         "backgroundColor": "", | ||||
|                         "border": "", | ||||
|                         "color": "" | ||||
|                     } | ||||
|                 }, | ||||
|                 "selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", | ||||
|                 "defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", | ||||
|                 "conditionSetIdentifier": { | ||||
|                     "namespace": "", | ||||
|                     "key": "035c589c-d98f-429e-8b89-d76bd8d22b29" | ||||
|                 } | ||||
|             }; | ||||
|             openmct.$injector = jasmine.createSpyObj('$injector', ['get']); | ||||
|             const mockTransactionService = jasmine.createSpyObj( | ||||
|                 'transactionService', | ||||
|                 ['commit'] | ||||
|             ); | ||||
|             openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]); | ||||
|             openmct.telemetry.isTelemetryObject.and.returnValue(true); | ||||
|             openmct.telemetry.subscribe.and.returnValue(function () {}); | ||||
|             openmct.telemetry.getValueFormatter.and.returnValue({ | ||||
|                 parse: function (value) { | ||||
|                     return value; | ||||
|                 } | ||||
|             }); | ||||
|             openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); | ||||
|             openmct.telemetry.request.and.returnValue(Promise.resolve([])); | ||||
|  | ||||
|             mockTransactionService.commit = async () => {}; | ||||
|             const mockIdentifierService = jasmine.createSpyObj( | ||||
|                 'identifierService', | ||||
|                 ['parse'] | ||||
|             ); | ||||
|             mockIdentifierService.parse.and.returnValue({ | ||||
|                 getSpace: () => { | ||||
|                     return ''; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             openmct.$injector = jasmine.createSpyObj('$injector', ['get']); | ||||
|             openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService) | ||||
|                 .withArgs('transactionService').and.returnValue(mockTransactionService); | ||||
|  | ||||
|             const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); | ||||
|             spyOn(styleRuleManger, 'subscribeToConditionSet'); | ||||
|             await openmct.editor.save(); | ||||
|             expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -29,9 +29,10 @@ const styleProps = { | ||||
|         noneValue: NONE_VALUE, | ||||
|         applicableForType: type => { | ||||
|             return !type ? true : (type === 'text-view' | ||||
|                                       || type === 'telemetry-view' | ||||
|                                       || type === 'box-view' | ||||
|                                       || type === 'subobject-view'); | ||||
|                                             || type === 'telemetry-view' | ||||
|                                             || type === 'box-view' | ||||
|                                             || type === 'ellipse-view' | ||||
|                                             || type === 'subobject-view'); | ||||
|         } | ||||
|     }, | ||||
|     border: { | ||||
| @@ -41,6 +42,7 @@ const styleProps = { | ||||
|             return !type ? true : (type === 'text-view' | ||||
|                                             || type === 'telemetry-view' | ||||
|                                             || type === 'box-view' | ||||
|                                             || type === 'ellipse-view' | ||||
|                                             || type === 'image-view' | ||||
|                                             || type === 'line-view' | ||||
|                                             || type === 'subobject-view'); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| <template> | ||||
| <component :is="urlDefined ? 'a' : 'span'" | ||||
|            class="c-condition-widget u-style-receiver js-style-receiver" | ||||
|            :href="urlDefined ? internalDomainObject.url : null" | ||||
|            :href="url" | ||||
| > | ||||
|     <div class="c-condition-widget__label"> | ||||
|         {{ internalDomainObject.label }} | ||||
| @@ -32,6 +32,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data: function () { | ||||
| @@ -42,6 +44,9 @@ export default { | ||||
|     computed: { | ||||
|         urlDefined() { | ||||
|             return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; | ||||
|         }, | ||||
|         url() { | ||||
|             return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|   | ||||
| @@ -149,6 +149,7 @@ define(['lodash'], function (_) { | ||||
|                         return type === 'text-view' | ||||
|                             || type === 'telemetry-view' | ||||
|                             || type === 'box-view' | ||||
|                             || type === 'ellipse-view' | ||||
|                             || type === 'image-view' | ||||
|                             || type === 'line-view' | ||||
|                             || type === 'subobject-view'; | ||||
| @@ -180,6 +181,10 @@ define(['lodash'], function (_) { | ||||
|                                     "name": "Box", | ||||
|                                     "class": "icon-box-round-corners" | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     "name": "Ellipse", | ||||
|                                     "class": "icon-circle" | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     "name": "Line", | ||||
|                                     "class": "icon-line-horz" | ||||
| @@ -745,7 +750,7 @@ define(['lodash'], function (_) { | ||||
|                         if (toolbar.remove.length === 0) { | ||||
|                             toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; | ||||
|                         } | ||||
|                     } else if (layoutItem.type === 'box-view') { | ||||
|                     } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { | ||||
|                         if (toolbar.position.length === 0) { | ||||
|                             toolbar.position = [ | ||||
|                                 getStackOrder(selectedParent, selectionPath), | ||||
|   | ||||
| @@ -43,7 +43,7 @@ import conditionalStylesMixin from '../mixins/objectStyles-mixin'; | ||||
| export default { | ||||
|     makeDefinition() { | ||||
|         return { | ||||
|             fill: '#717171', | ||||
|             fill: '#666666', | ||||
|             stroke: '', | ||||
|             x: 1, | ||||
|             y: 1, | ||||
|   | ||||
| @@ -76,6 +76,7 @@ import uuid from 'uuid'; | ||||
| import SubobjectView from './SubobjectView.vue'; | ||||
| import TelemetryView from './TelemetryView.vue'; | ||||
| import BoxView from './BoxView.vue'; | ||||
| import EllipseView from './EllipseView.vue'; | ||||
| import TextView from './TextView.vue'; | ||||
| import LineView from './LineView.vue'; | ||||
| import ImageView from './ImageView.vue'; | ||||
| @@ -112,6 +113,7 @@ const ITEM_TYPE_VIEW_MAP = { | ||||
|     'subobject-view': SubobjectView, | ||||
|     'telemetry-view': TelemetryView, | ||||
|     'box-view': BoxView, | ||||
|     'ellipse-view': EllipseView, | ||||
|     'line-view': LineView, | ||||
|     'text-view': TextView, | ||||
|     'image-view': ImageView | ||||
|   | ||||
| @@ -28,19 +28,19 @@ | ||||
| > | ||||
|     <div | ||||
|         class="c-frame-edit__handle c-frame-edit__handle--nw" | ||||
|         @mousedown="startResize([1,1], [-1,-1], $event)" | ||||
|         @mousedown.left="startResize([1,1], [-1,-1], $event)" | ||||
|     ></div> | ||||
|     <div | ||||
|         class="c-frame-edit__handle c-frame-edit__handle--ne" | ||||
|         @mousedown="startResize([0,1], [1,-1], $event)" | ||||
|         @mousedown.left="startResize([0,1], [1,-1], $event)" | ||||
|     ></div> | ||||
|     <div | ||||
|         class="c-frame-edit__handle c-frame-edit__handle--sw" | ||||
|         @mousedown="startResize([1,0], [-1,1], $event)" | ||||
|         @mousedown.left="startResize([1,0], [-1,1], $event)" | ||||
|     ></div> | ||||
|     <div | ||||
|         class="c-frame-edit__handle c-frame-edit__handle--se" | ||||
|         @mousedown="startResize([0,0], [1,1], $event)" | ||||
|         @mousedown.left="startResize([0,0], [1,1], $event)" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
							
								
								
									
										122
									
								
								src/plugins/displayLayout/components/EllipseView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/plugins/displayLayout/components/EllipseView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| /***************************************************************************** | ||||
| * 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> | ||||
| <layout-frame | ||||
|     :item="item" | ||||
|     :grid-size="gridSize" | ||||
|     :is-editing="isEditing" | ||||
|     @move="(gridDelta) => $emit('move', gridDelta)" | ||||
|     @endMove="() => $emit('endMove')" | ||||
| > | ||||
|     <div | ||||
|         class="c-ellipse-view u-style-receiver js-style-receiver" | ||||
|         :class="[styleClass]" | ||||
|         :style="style" | ||||
|     ></div> | ||||
| </layout-frame> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import LayoutFrame from './LayoutFrame.vue'; | ||||
| import conditionalStylesMixin from '../mixins/objectStyles-mixin'; | ||||
|  | ||||
| export default { | ||||
|     makeDefinition() { | ||||
|         return { | ||||
|             fill: '#666666', | ||||
|             stroke: '', | ||||
|             x: 1, | ||||
|             y: 1, | ||||
|             width: 10, | ||||
|             height: 10 | ||||
|         }; | ||||
|     }, | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         gridSize: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             validator: (arr) => arr && arr.length === 2 | ||||
|                     && arr.every(el => typeof el === 'number') | ||||
|         }, | ||||
|         index: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         initSelect: Boolean, | ||||
|         isEditing: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         style() { | ||||
|             if (this.itemStyle) { | ||||
|                 return this.itemStyle; | ||||
|             } else { | ||||
|                 return { | ||||
|                     backgroundColor: this.item.fill, | ||||
|                     border: this.item.stroke ? '1px solid ' + this.item.stroke : '' | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         index(newIndex) { | ||||
|             if (!this.context) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.context.index = newIndex; | ||||
|         }, | ||||
|         item(newItem) { | ||||
|             if (!this.context) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.context.layoutItem = newItem; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.context = { | ||||
|             layoutItem: this.item, | ||||
|             index: this.index | ||||
|         }; | ||||
|         this.removeSelectable = this.openmct.selection.selectable( | ||||
|             this.$el, this.context, this.initSelect); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -33,7 +33,7 @@ | ||||
|     <slot></slot> | ||||
|     <div | ||||
|         class="c-frame__move-bar" | ||||
|         @mousedown="isEditing ? startMove([1,1], [0,0], $event) : null" | ||||
|         @mousedown.left="startMove($event)" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -93,7 +93,11 @@ export default { | ||||
|                 return value - this.initialPosition[index]; | ||||
|             }.bind(this)); | ||||
|         }, | ||||
|         startMove(posFactor, dimFactor, event) { | ||||
|         startMove(event, posFactor = [1, 1], dimFactor = [0, 0]) { | ||||
|             if (!this.isEditing) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             document.body.addEventListener('mousemove', this.continueMove); | ||||
|             document.body.addEventListener('mouseup', this.endMove); | ||||
|             this.dragPosition = { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user