Compare commits
	
		
			35 Commits
		
	
	
		
	
	| 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 | 
| @@ -118,100 +118,6 @@ define([ | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         'example.spectral-generator': { | ||||
|             values: [ | ||||
|                 { | ||||
|                     key: "name", | ||||
|                     name: "Name", | ||||
|                     format: "string" | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "utc", | ||||
|                     name: "Time", | ||||
|                     format: "utc", | ||||
|                     hints: { | ||||
|                         domain: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "wavelength", | ||||
|                     name: "Wavelength", | ||||
|                     unit: "Hz", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         domain: 2, | ||||
|                         spectralAttribute: true | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         range: 2, | ||||
|                         spectralAttribute: true | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         'example.spectral-aggregate-generator': { | ||||
|             values: [ | ||||
|                 { | ||||
|                     key: "name", | ||||
|                     name: "Name", | ||||
|                     format: "string" | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "utc", | ||||
|                     name: "Time", | ||||
|                     format: "utc", | ||||
|                     hints: { | ||||
|                         domain: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch1", | ||||
|                     name: "Channel 1", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch2", | ||||
|                     name: "Channel 2", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch3", | ||||
|                     name: "Channel 3", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 3 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch4", | ||||
|                     name: "Channel 4", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 4 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch5", | ||||
|                     name: "Channel 5", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 5 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -1,86 +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([ | ||||
|  | ||||
| ], function ( | ||||
|  | ||||
| ) { | ||||
|  | ||||
|     function SpectralAggregateGeneratorProvider() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function pointForTimestamp(timestamp, count, name) { | ||||
|         return { | ||||
|             name: name, | ||||
|             utc: String(Math.floor(timestamp / count) * count), | ||||
|             ch1: String(Math.floor(timestamp / count) % 1), | ||||
|             ch2: String(Math.floor(timestamp / count) % 2), | ||||
|             ch3: String(Math.floor(timestamp / count) % 3), | ||||
|             ch4: String(Math.floor(timestamp / count) % 4), | ||||
|             ch5: String(Math.floor(timestamp / count) % 5) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) { | ||||
|         return domainObject.type === 'example.spectral-aggregate-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var count = 5000; | ||||
|  | ||||
|         var interval = setInterval(function () { | ||||
|             var now = Date.now(); | ||||
|             var datum = pointForTimestamp(now, count, domainObject.name); | ||||
|             callback(datum); | ||||
|         }, count); | ||||
|  | ||||
|         return function () { | ||||
|             clearInterval(interval); | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) { | ||||
|         return domainObject.type === 'example.spectral-aggregate-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) { | ||||
|         var start = options.start; | ||||
|         var end = Math.min(Date.now(), options.end); // no future values | ||||
|         var count = 5000; | ||||
|         if (options.strategy === 'latest' || options.size === 1) { | ||||
|             start = end; | ||||
|         } | ||||
|  | ||||
|         var data = []; | ||||
|         while (start <= end && data.length < 5000) { | ||||
|             data.push(pointForTimestamp(start, count, domainObject.name)); | ||||
|             start += count; | ||||
|         } | ||||
|  | ||||
|         return Promise.resolve(data); | ||||
|     }; | ||||
|  | ||||
|     return SpectralAggregateGeneratorProvider; | ||||
|  | ||||
| }); | ||||
| @@ -1,102 +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([ | ||||
|     './WorkerInterface' | ||||
| ], function ( | ||||
|     WorkerInterface | ||||
| ) { | ||||
|  | ||||
|     var REQUEST_DEFAULTS = { | ||||
|         amplitude: 1, | ||||
|         wavelength: 1, | ||||
|         period: 10, | ||||
|         offset: 0, | ||||
|         dataRateInHz: 1, | ||||
|         randomness: 0, | ||||
|         phase: 0 | ||||
|     }; | ||||
|  | ||||
|     function SpectralGeneratorProvider() { | ||||
|         this.workerInterface = new WorkerInterface(); | ||||
|     } | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         return domainObject.type === 'example.spectral-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.supportsRequest = | ||||
|         SpectralGeneratorProvider.prototype.supportsSubscribe = | ||||
|             SpectralGeneratorProvider.prototype.canProvideTelemetry; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) { | ||||
|         var props = [ | ||||
|             'amplitude', | ||||
|             'wavelength', | ||||
|             'period', | ||||
|             'offset', | ||||
|             'dataRateInHz', | ||||
|             'phase', | ||||
|             'randomness' | ||||
|         ]; | ||||
|  | ||||
|         var workerRequest = {}; | ||||
|  | ||||
|         props.forEach(function (prop) { | ||||
|             if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) { | ||||
|                 workerRequest[prop] = domainObject.telemetry[prop]; | ||||
|             } | ||||
|  | ||||
|             if (request && Object.prototype.hasOwnProperty.call(request, prop)) { | ||||
|                 workerRequest[prop] = request[prop]; | ||||
|             } | ||||
|  | ||||
|             if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) { | ||||
|                 workerRequest[prop] = REQUEST_DEFAULTS[prop]; | ||||
|             } | ||||
|  | ||||
|             workerRequest[prop] = Number(workerRequest[prop]); | ||||
|         }); | ||||
|  | ||||
|         workerRequest.name = domainObject.name; | ||||
|  | ||||
|         return workerRequest; | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.request = function (domainObject, request) { | ||||
|         var workerRequest = this.makeWorkerRequest(domainObject, request); | ||||
|         workerRequest.start = request.start; | ||||
|         workerRequest.end = request.end; | ||||
|         workerRequest.spectra = true; | ||||
|  | ||||
|         return this.workerInterface.request(workerRequest); | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var workerRequest = this.makeWorkerRequest(domainObject, {}); | ||||
|         workerRequest.spectra = true; | ||||
|  | ||||
|         return this.workerInterface.subscribe(workerRequest, callback); | ||||
|     }; | ||||
|  | ||||
|     return SpectralGeneratorProvider; | ||||
| }); | ||||
| @@ -24,15 +24,11 @@ define([ | ||||
|     "./GeneratorProvider", | ||||
|     "./SinewaveLimitProvider", | ||||
|     "./StateGeneratorProvider", | ||||
|     "./SpectralGeneratorProvider", | ||||
|     "./SpectralAggregateGeneratorProvider", | ||||
|     "./GeneratorMetadataProvider" | ||||
| ], function ( | ||||
|     GeneratorProvider, | ||||
|     SinewaveLimitProvider, | ||||
|     StateGeneratorProvider, | ||||
|     SpectralGeneratorProvider, | ||||
|     SpectralAggregateGeneratorProvider, | ||||
|     GeneratorMetadataProvider | ||||
| ) { | ||||
|  | ||||
| @@ -65,37 +61,6 @@ define([ | ||||
|  | ||||
|         openmct.telemetry.addProvider(new StateGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("example.spectral-generator", { | ||||
|             name: "Spectral Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", | ||||
|             cssClass: "icon-generator-telemetry", | ||||
|             creatable: true, | ||||
|             initialize: function (object) { | ||||
|                 object.telemetry = { | ||||
|                     period: 10, | ||||
|                     amplitude: 1, | ||||
|                     wavelength: 1, | ||||
|                     frequency: 1, | ||||
|                     offset: 0, | ||||
|                     dataRateInHz: 1, | ||||
|                     phase: 0, | ||||
|                     randomness: 0 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|         openmct.telemetry.addProvider(new SpectralGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("example.spectral-aggregate-generator", { | ||||
|             name: "Spectral Aggregate Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple state algorithm.", | ||||
|             cssClass: "icon-generator-telemetry", | ||||
|             creatable: true, | ||||
|             initialize: function (object) { | ||||
|                 object.telemetry = {}; | ||||
|             } | ||||
|         }); | ||||
|         openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("generator", { | ||||
|             name: "Sine Wave Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.8-SNAPSHOT", | ||||
|   "version": "1.7.8", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@braintree/sanitize-url": "^5.0.2", | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
|     "babel-eslint": "10.0.3", | ||||
|   | ||||
| @@ -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(); | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -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()); | ||||
|   | ||||
| @@ -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)); | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -110,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]); | ||||
|         } | ||||
|   | ||||
							
								
								
									
										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); | ||||
|   | ||||
| @@ -483,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); | ||||
|   | ||||
| @@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         this.parseTime = undefined; | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
|         this.unsubscribe = undefined; | ||||
|         this.historicalProvider = undefined; | ||||
|         this.options = options; | ||||
|         this.pageState = undefined; | ||||
|         this.lastBounds = undefined; | ||||
| @@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter { | ||||
|             this._error(ERRORS.LOADED); | ||||
|         } | ||||
|  | ||||
|         this._timeSystem(this.openmct.time.timeSystem()); | ||||
|         this._setTimeSystem(this.openmct.time.timeSystem()); | ||||
|         this.lastBounds = this.openmct.time.bounds(); | ||||
|  | ||||
|         this._watchBounds(); | ||||
|         this._watchTimeSystem(); | ||||
|  | ||||
|         this._initiateHistoricalRequests(); | ||||
|         this._requestHistoricalTelemetry(); | ||||
|         this._initiateSubscriptionTelemetry(); | ||||
|  | ||||
|         this.loaded = true; | ||||
| @@ -103,36 +102,35 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         return this.boundedTelemetry; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets up  the telemetry collection for historical requests, | ||||
|      * this uses the "standardizeRequestOptions" from Telemetry API | ||||
|      * @private | ||||
|      */ | ||||
|     _initiateHistoricalRequests() { | ||||
|         this.openmct.telemetry.standardizeRequestOptions(this.options); | ||||
|         this.historicalProvider = this.openmct.telemetry. | ||||
|             findRequestProvider(this.domainObject, this.options); | ||||
|  | ||||
|         this._requestHistoricalTelemetry(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If a historical provider exists, then historical requests will be made | ||||
|      * @private | ||||
|      */ | ||||
|     async _requestHistoricalTelemetry() { | ||||
|         if (!this.historicalProvider) { | ||||
|         let options = { ...this.options }; | ||||
|         let historicalProvider; | ||||
|  | ||||
|         this.openmct.telemetry.standardizeRequestOptions(options); | ||||
|         historicalProvider = this.openmct.telemetry. | ||||
|             findRequestProvider(this.domainObject, options); | ||||
|  | ||||
|         if (!historicalProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let historicalData; | ||||
|  | ||||
|         this.options.onPartialResponse = this._processNewTelemetry.bind(this); | ||||
|         options.onPartialResponse = this._processNewTelemetry.bind(this); | ||||
|  | ||||
|         try { | ||||
|             if (this.requestAbort) { | ||||
|                 this.requestAbort.abort(); | ||||
|             } | ||||
|  | ||||
|             this.requestAbort = new AbortController(); | ||||
|             this.options.signal = this.requestAbort.signal; | ||||
|             historicalData = await this.historicalProvider.request(this.domainObject, this.options); | ||||
|             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...'); | ||||
| @@ -140,6 +138,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.emit('requestEnded'); | ||||
|         this.requestAbort = undefined; | ||||
|  | ||||
|         this._processNewTelemetry(historicalData); | ||||
| @@ -173,6 +172,10 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _processNewTelemetry(telemetryData) { | ||||
|         if (telemetryData === undefined) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; | ||||
|         let parsedValue; | ||||
|         let beforeStartOfBounds; | ||||
| @@ -199,9 +202,10 @@ export class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|                     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) { | ||||
| @@ -317,7 +321,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * Time System | ||||
|      * @private | ||||
|      */ | ||||
|     _timeSystem(timeSystem) { | ||||
|     _setTimeSystem(timeSystem) { | ||||
|         let domains = this.metadata.valuesForHints(['domain']); | ||||
|         let domain = domains.find((d) => d.key === timeSystem.key); | ||||
|  | ||||
| @@ -333,7 +337,10 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         this.parseTime = (datum) => { | ||||
|             return valueFormatter.parse(datum); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     _setTimeSystemAndFetchData(timeSystem) { | ||||
|         this._setTimeSystem(timeSystem); | ||||
|         this._reset(); | ||||
|     } | ||||
|  | ||||
| @@ -370,19 +377,19 @@ export class TelemetryCollection extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * adds the _timeSystem callback to the 'timeSystem' timeAPI listener | ||||
|      * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _watchTimeSystem() { | ||||
|         this.openmct.time.on('timeSystem', this._timeSystem, this); | ||||
|         this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * removes the _timeSystem callback from the 'timeSystem' timeAPI listener | ||||
|      * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener | ||||
|      * @private | ||||
|      */ | ||||
|     _unwatchTimeSystem() { | ||||
|         this.openmct.time.off('timeSystem', this._timeSystem, this); | ||||
|         this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -172,7 +172,7 @@ class TimeAPI extends GlobalTimeContext { | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getContextForView | ||||
|      */ | ||||
|     getContextForView(objectPath) { | ||||
|     getContextForView(objectPath = []) { | ||||
|         let timeContext = this; | ||||
|  | ||||
|         objectPath.forEach(item => { | ||||
|   | ||||
| @@ -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(() => { | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -23,30 +23,26 @@ | ||||
| import { BAR_GRAPH_KEY } from './BarGraphConstants'; | ||||
| 
 | ||||
| export default function BarGraphCompositionPolicy(openmct) { | ||||
|     function hasAggregateDomainAndRange(metadata) { | ||||
|     function hasRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
| 
 | ||||
|         return rangeValues.length > 0; | ||||
|     } | ||||
| 
 | ||||
|     function hasBarGraphTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|         if (!openmct.telemetry.isTelemetryObject(domainObject)) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
| 
 | ||||
|         return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata); | ||||
|     } | ||||
| 
 | ||||
|     function hasNoChildren(parentObject) { | ||||
|         return parentObject.composition && parentObject.composition.length < 1; | ||||
|         return metadata.values().length > 0 && hasRange(metadata); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         allow: function (parent, child) { | ||||
|             if ((parent.type === BAR_GRAPH_KEY) | ||||
|                 && ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false)) | ||||
|                 && (hasBarGraphTelemetry(child) === false) | ||||
|             ) { | ||||
|                 return false; | ||||
|             } | ||||
| @@ -1,5 +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'; | ||||
| export const SUBSCRIBE = 'subscribe'; | ||||
| export const UNSUBSCRIBE = 'unsubscribe'; | ||||
| @@ -12,6 +12,7 @@ | ||||
|     </div> | ||||
|     <div ref="plot" | ||||
|          class="c-bar-chart" | ||||
|          @plotly_relayout="zoom" | ||||
|     ></div> | ||||
|     <div v-if="false" | ||||
|          ref="localControl" | ||||
| @@ -28,8 +29,7 @@ | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
| import Plotly from 'plotly.js-basic-dist'; | ||||
| import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants'; | ||||
| import Plotly from 'plotly-basic'; | ||||
| 
 | ||||
| const MULTI_AXES_X_PADDING_PERCENT = { | ||||
|     LEFT: 8, | ||||
| @@ -79,8 +79,6 @@ export default { | ||||
|         this.registerListeners(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.$refs.plot.removeAllListeners(); | ||||
| 
 | ||||
|         if (this.plotResizeObserver) { | ||||
|             this.plotResizeObserver.unobserve(this.$refs.plotWrapper); | ||||
|             clearTimeout(this.resizeTimer); | ||||
| @@ -139,8 +137,8 @@ export default { | ||||
|         getYAxisMeta() { | ||||
|             const yAxisMeta = {}; | ||||
| 
 | ||||
|             this.data.forEach(d => { | ||||
|                 const yAxisMetadata = d.yAxisMetadata; | ||||
|             this.data.forEach(datum => { | ||||
|                 const yAxisMetadata = datum.yAxisMetadata; | ||||
|                 const range = '1'; | ||||
|                 const side = 'left'; | ||||
|                 const name = ''; | ||||
| @@ -203,8 +201,6 @@ export default { | ||||
|             return yaxis; | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.$refs.plot.on('plotly_relayout', this.zoom); | ||||
| 
 | ||||
|             this.removeBarColorListener = this.openmct.objects.observe( | ||||
|                 this.domainObject, | ||||
|                 'configuration.barStyles', | ||||
| @@ -226,17 +222,17 @@ export default { | ||||
|             this.updatePlot(); | ||||
| 
 | ||||
|             this.isZoomed = false; | ||||
|             this.$emit(SUBSCRIBE); | ||||
|             this.$emit('subscribe'); | ||||
|         }, | ||||
|         barColorChanged() { | ||||
|             const colors = []; | ||||
|             const indices = []; | ||||
|             this.data.forEach((item, index) => { | ||||
|                 const key = item.key; | ||||
|                 const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color; | ||||
|                 const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color; | ||||
|                 indices.push(index); | ||||
|                 if (color) { | ||||
|                     colors.push(); | ||||
|                 if (colorExists) { | ||||
|                     colors.push(this.domainObject.configuration.barStyles.series[key].color); | ||||
|                 } else { | ||||
|                     colors.push(item.marker.color); | ||||
|                 } | ||||
| @@ -285,7 +281,7 @@ export default { | ||||
|             } | ||||
| 
 | ||||
|             this.isZoomed = true; | ||||
|             this.$emit(UNSUBSCRIBE); | ||||
|             this.$emit('unsubscribe'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -25,33 +25,31 @@ | ||||
|           class="c-plot c-bar-chart-view" | ||||
|           :data="trace" | ||||
|           :plot-axis-title="plotAxisTitle" | ||||
|           @subscribe="subscribeToAll" | ||||
|           @unsubscribe="removeAllSubscriptions" | ||||
| /> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import * as SPECTRAL_AGGREGATE from './BarGraphConstants'; | ||||
| import ColorPalette from '../lib/ColorPalette'; | ||||
| import BarGraph from './BarGraphPlot.vue'; | ||||
| import Color from "@/plugins/plot/lib/Color"; | ||||
| import _ from 'lodash'; | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
|         BarGraph | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     data() { | ||||
|         this.telemetryObjects = {}; | ||||
|         this.telemetryObjectFormats = {}; | ||||
|         this.subscriptions = []; | ||||
|         this.composition = {}; | ||||
| 
 | ||||
|         return { | ||||
|             composition: {}, | ||||
|             currentDomainObject: this.domainObject, | ||||
|             subscriptions: [], | ||||
|             telemetryObjects: {}, | ||||
|             trace: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         activeClock() { | ||||
|             return this.openmct.time.activeClock; | ||||
|         }, | ||||
|         plotAxisTitle() { | ||||
|             const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; | ||||
|             const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; | ||||
| @@ -64,24 +62,14 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|         this.loadComposition(); | ||||
| 
 | ||||
|         this.openmct.time.on('bounds', this.refreshData); | ||||
|         this.openmct.time.on('clock', this.clockChanged); | ||||
| 
 | ||||
|         this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll); | ||||
|         this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions); | ||||
| 
 | ||||
|         this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.$refs.barGraph.$off(); | ||||
|         this.openmct.time.off('bounds', this.refreshData); | ||||
|         this.openmct.time.off('clock', this.clockChanged); | ||||
| 
 | ||||
|         this.removeAllSubscriptions(); | ||||
|         this.unobserve(); | ||||
| 
 | ||||
|         if (!this.composition) { | ||||
|             return; | ||||
| @@ -92,35 +80,34 @@ export default { | ||||
|     }, | ||||
|     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); | ||||
| 
 | ||||
|             if (!this.domainObject.configuration.barStyles) { | ||||
|                 this.domainObject.configuration.barStyles = {}; | ||||
|             // 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]); | ||||
|             } | ||||
| 
 | ||||
|             // check to see if we've set a bar color | ||||
|             if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) { | ||||
|                 const color = this.colorPalette.getNextColor().asHexString(); | ||||
|                 this.domainObject.configuration.barStyles[key] = { | ||||
|                     name: telemetryObject.name, | ||||
|                     color | ||||
|                 }; | ||||
|             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[${this.key}]`, | ||||
|                     this.domainObject.configuration.barStyles[key] | ||||
|                     `configuration.barStyles.series["${key}"]`, | ||||
|                     stylesUpdate | ||||
|                 ); | ||||
|             } else { | ||||
|                 let color = this.domainObject.configuration.barStyles[key].color; | ||||
|                 if (!(color instanceof Color)) { | ||||
|                     color = Color.fromHexString(color); | ||||
|                 } | ||||
| 
 | ||||
|                 this.colorPalette.remove(color); | ||||
|             } | ||||
| 
 | ||||
|             this.telemetryObjects[key] = telemetryObject; | ||||
| 
 | ||||
|             // ask for the current telemetry data, then subcribe for changes | ||||
|             this.requestDataFor(telemetryObject); | ||||
|             this.subscribeToObject(telemetryObject); | ||||
|         }, | ||||
| @@ -144,12 +131,12 @@ export default { | ||||
| 
 | ||||
|             this.trace = isInTrace ? newTrace : newTrace.concat([trace]); | ||||
|         }, | ||||
|         clockChanged() { | ||||
|             this.removeAllSubscriptions(); | ||||
|             this.subscribeToAll(); | ||||
|         }, | ||||
|         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']); | ||||
| @@ -159,21 +146,19 @@ export default { | ||||
|                 yAxisMetadata | ||||
|             }; | ||||
|         }, | ||||
|         getOptions(telemetryObject) { | ||||
|         getOptions() { | ||||
|             const { start, end } = this.openmct.time.bounds(); | ||||
| 
 | ||||
|             return { | ||||
|                 end, | ||||
|                 start, | ||||
|                 startTime: null, | ||||
|                 spectra: true | ||||
|                 start | ||||
|             }; | ||||
|         }, | ||||
|         loadComposition() { | ||||
|             this.composition = this.openmct.composition.get(this.currentDomainObject); | ||||
|             this.composition = this.openmct.composition.get(this.domainObject); | ||||
| 
 | ||||
|             if (!this.composition) { | ||||
|                 this.addTelemetryObject(this.currentDomainObject); | ||||
|                 this.addTelemetryObject(this.domainObject); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| @@ -202,21 +187,34 @@ export default { | ||||
|         removeTelemetryObject(identifier) { | ||||
|             const key = this.openmct.objects.makeKeyString(identifier); | ||||
|             delete this.telemetryObjects[key]; | ||||
|             if (this.domainObject.configuration.barStyles[key]) { | ||||
|                 delete this.domainObject.configuration.barStyles[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); | ||||
|         }, | ||||
|         processData(telemetryObject, data, axisMetadata) { | ||||
|         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 = []; | ||||
| 
 | ||||
| @@ -224,10 +222,10 @@ export default { | ||||
|             axisMetadata.xAxisMetadata.forEach((metadata) => { | ||||
|                 xValues.push(metadata.name); | ||||
|                 if (data[metadata.key]) { | ||||
|                     //TODO: Format the data? | ||||
|                     yValues.push(data[metadata.key]); | ||||
|                     const formattedValue = this.format(key, metadata.key, data); | ||||
|                     yValues.push(formattedValue); | ||||
|                 } else { | ||||
|                     yValues.push(''); | ||||
|                     yValues.push(null); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
| @@ -241,20 +239,43 @@ export default { | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: 'bar', | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles[key].color | ||||
|                     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, this.getOptions(telemetryObject)) | ||||
|             this.openmct.telemetry.request(telemetryObject) | ||||
|                 .then(data => { | ||||
|                     data.forEach((datum) => { | ||||
|                         this.processData(telemetryObject, datum, axisMetadata); | ||||
|                         this.addDataToGraph(telemetryObject, datum, axisMetadata); | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     console.warn(`Error fetching data`, error); | ||||
|                 }); | ||||
|         }, | ||||
|         subscribeToObject(telemetryObject) { | ||||
| @@ -262,10 +283,10 @@ export default { | ||||
| 
 | ||||
|             this.removeSubscription(key); | ||||
| 
 | ||||
|             const options = this.getOptions(telemetryObject); | ||||
|             const options = this.getOptions(); | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, | ||||
|                 data => this.processData(telemetryObject, data, axisMetadata) | ||||
|                 data => this.addDataToGraph(telemetryObject, data, axisMetadata) | ||||
|                 , options); | ||||
| 
 | ||||
|             this.subscriptions.push({ | ||||
| @@ -276,9 +297,6 @@ export default { | ||||
|         subscribeToAll() { | ||||
|             const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|             telemetryObjects.forEach(this.subscribeToObject); | ||||
|         }, | ||||
|         updateDomainObject(newDomainObject) { | ||||
|             this.currentDomainObject = newDomainObject; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -26,12 +26,14 @@ import Vue from 'vue'; | ||||
| 
 | ||||
| export default function BarGraphViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
| 
 | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         key: BAR_GRAPH_VIEW, | ||||
|         name: 'Spectral Aggregate Plot', | ||||
|         name: 'Bar Graph', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === BAR_GRAPH_KEY; | ||||
| @@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) { | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; | ||||
| import Vue from 'vue'; | ||||
| import Options from "./Options.vue"; | ||||
| import BarGraphOptions from "./BarGraphOptions.vue"; | ||||
| 
 | ||||
| export default function BarGraphInspectorViewProvider(openmct) { | ||||
|     return { | ||||
| @@ -24,13 +24,13 @@ export default function BarGraphInspectorViewProvider(openmct) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             Options | ||||
|                             BarGraphOptions | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject: selection[0][0].context.item | ||||
|                         }, | ||||
|                         template: '<options></options>' | ||||
|                         template: '<bar-graph-options></bar-graph-options>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
| @@ -20,27 +20,31 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <template> | ||||
| <div> | ||||
|     <ul class="c-tree"> | ||||
|         <li v-for="series in domainObject.composition" | ||||
|             :key="series.key" | ||||
|         > | ||||
|             <bar-graph-options :item="series" /> | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| <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 BarGraphOptions from "./BarGraphOptions.vue"; | ||||
| import SeriesOptions from "./SeriesOptions.vue"; | ||||
| import ColorPalette from '@/ui/color/ColorPalette'; | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
|         BarGraphOptions | ||||
|         SeriesOptions | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing() | ||||
|             isEditing: this.openmct.editor.isEditing(), | ||||
|             colorPalette: this.colorPalette | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -48,6 +52,9 @@ export default { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
| @@ -21,21 +21,26 @@ | ||||
| --> | ||||
| <template> | ||||
| <ul> | ||||
|     <li class="c-tree__item menus-to-left"> | ||||
|     <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> | ||||
| 
 | ||||
|         <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." | ||||
|                  edit-title="Manually set the color for this bar graph" | ||||
|                  view-title="The color for this bar graph." | ||||
|                  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" | ||||
| @@ -44,7 +49,8 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ColorSwatch from '../../ColorSwatch.vue'; | ||||
| import ColorSwatch from '@/ui/color/ColorSwatch.vue'; | ||||
| import Color from "@/ui/color/Color"; | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
| @@ -55,50 +61,90 @@ export default { | ||||
|         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.initColor(); | ||||
|                 this.initColorAndName(); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.key = this.openmct.objects.makeKeyString(this.item); | ||||
|         this.initColor(); | ||||
|         this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor); | ||||
|         this.initColorAndName(); | ||||
|         this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unObserve) { | ||||
|             this.unObserve(); | ||||
|         if (this.removeBarStylesListener) { | ||||
|             this.removeBarStylesListener(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         initColor() { | ||||
|             if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) { | ||||
|                 this.currentColor = this.domainObject.configuration.barStyles[this.key].color; | ||||
|                 this.name = this.domainObject.configuration.barStyles[this.key].name; | ||||
|         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[${this.key}].color`, | ||||
|                 `configuration.barStyles.series["${this.key}"].color`, | ||||
|                 this.currentColor | ||||
|             ); | ||||
|         } | ||||
							
								
								
									
										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'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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', () => { | ||||
| @@ -722,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); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -101,7 +101,7 @@ export default { | ||||
|         addChildren(domainObject) { | ||||
|             let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             let metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
|             let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters); | ||||
|             let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : []; | ||||
|             let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined; | ||||
|             let mutateFilters = false; | ||||
|             let childObject = { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
|        'c-hyperlink--button' : isButton | ||||
|    }" | ||||
|    :target="domainObject.linkTarget" | ||||
|    :href="domainObject.url" | ||||
|    :href="url" | ||||
| > | ||||
|     <span class="c-hyperlink__label">{{ domainObject.displayText }}</span> | ||||
| </a> | ||||
| @@ -35,6 +35,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl; | ||||
|  | ||||
| export default { | ||||
|     inject: ['domainObject'], | ||||
| @@ -45,6 +46,9 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         }, | ||||
|         url() { | ||||
|             return sanitizeUrl(this.domainObject.url); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -66,6 +66,10 @@ export default function ImageryTimestripViewProvider(openmct) { | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 }, | ||||
|  | ||||
|                 getComponent() { | ||||
|                     return component; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|   | ||||
| @@ -10,7 +10,14 @@ export default class ImageryView { | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(element) { | ||||
|     show(element, isEditing, viewOptions) { | ||||
|         let alternateObjectPath; | ||||
|         let indexForFocusedImage; | ||||
|         if (viewOptions) { | ||||
|             indexForFocusedImage = viewOptions.indexForFocusedImage; | ||||
|             alternateObjectPath = viewOptions.objectPath; | ||||
|         } | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
| @@ -19,10 +26,15 @@ export default class ImageryView { | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 domainObject: this.domainObject, | ||||
|                 objectPath: this.objectPath, | ||||
|                 objectPath: alternateObjectPath || this.objectPath, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             template: '<imagery-view ref="ImageryContainer"></imagery-view>' | ||||
|             data() { | ||||
|                 return { | ||||
|                     indexForFocusedImage | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>' | ||||
|  | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -41,7 +41,13 @@ import _ from "lodash"; | ||||
|  | ||||
| const PADDING = 1; | ||||
| const ROW_HEIGHT = 100; | ||||
| const IMAGE_WIDTH_THRESHOLD = 40; | ||||
| const IMAGE_SIZE = 85; | ||||
| const IMAGE_WIDTH_THRESHOLD = 25; | ||||
| const CONTAINER_CLASS = 'c-imagery-tsv-container'; | ||||
| const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items'; | ||||
| const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper'; | ||||
| const ID_PREFIX = 'wrapper-'; | ||||
| const IMAGE_ID_PREFIX = 'image-'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [imageryData], | ||||
| @@ -78,10 +84,12 @@ export default { | ||||
|         this.canvasContext = this.canvas.getContext('2d'); | ||||
|         this.setDimensions(); | ||||
|  | ||||
|         this.updateViewBounds(); | ||||
|         this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this); | ||||
|         this.updateViewBounds = this.updateViewBounds.bind(this); | ||||
|         this.setTimeContext = this.setTimeContext.bind(this); | ||||
|         this.setTimeContext(); | ||||
|  | ||||
|         this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|         this.updateViewBounds(); | ||||
|  | ||||
|         this.resize = _.debounce(this.resize, 400); | ||||
|         this.imageryStripResizeObserver = new ResizeObserver(this.resize); | ||||
| @@ -90,25 +98,36 @@ export default { | ||||
|         this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         if (this.imageryStripResizeObserver) { | ||||
|             this.imageryStripResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery); | ||||
|         this.openmct.time.off("bounds", this.updateViewBounds); | ||||
|         this.stopFollowingTimeContext(); | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
|             this.timeContext.on("timeSystem", this.setScaleAndPlotImagery); | ||||
|             this.timeContext.on("bounds", this.updateViewBounds); | ||||
|             this.timeContext.on("timeContext", this.setTimeContext); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off("timeSystem", this.setScaleAndPlotImagery); | ||||
|                 this.timeContext.off("bounds", this.updateViewBounds); | ||||
|                 this.timeContext.off("timeContext", this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         expand(index) { | ||||
|             const path = this.objectPath[0]; | ||||
|             this.previewAction.invoke([path]); | ||||
|             this.previewAction.invoke([path], { | ||||
|                 indexForFocusedImage: index, | ||||
|                 objectPath: this.objectPath | ||||
|             }); | ||||
|         }, | ||||
|         observeForChanges(mutatedObject) { | ||||
|             this.updateViewBounds(); | ||||
| @@ -134,14 +153,10 @@ export default { | ||||
|             return clientWidth; | ||||
|         }, | ||||
|         updateViewBounds(bounds, isTick) { | ||||
|             this.viewBounds = this.openmct.time.bounds(); | ||||
|             //Add a 50% padding to the end bounds to look ahead | ||||
|             let timespan = (this.viewBounds.end - this.viewBounds.start); | ||||
|             let padding = timespan / 2; | ||||
|             this.viewBounds.end = this.viewBounds.end + padding; | ||||
|             this.viewBounds = this.timeContext.bounds(); | ||||
|  | ||||
|             if (this.timeSystem === undefined) { | ||||
|                 this.timeSystem = this.openmct.time.timeSystem(); | ||||
|                 this.timeSystem = this.timeContext.timeSystem(); | ||||
|             } | ||||
|  | ||||
|             this.setScaleAndPlotImagery(this.timeSystem, !isTick); | ||||
| @@ -172,18 +187,18 @@ export default { | ||||
|         }, | ||||
|         clearPreviousImagery(clearAllImagery) { | ||||
|             //TODO: Only clear items that are out of bounds | ||||
|             let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items"); | ||||
|             let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`); | ||||
|             noItemsEl.forEach(item => { | ||||
|                 item.remove(); | ||||
|             }); | ||||
|             let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper"); | ||||
|             let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`); | ||||
|             imagery.forEach(item => { | ||||
|                 if (clearAllImagery) { | ||||
|                     item.remove(); | ||||
|                 } else { | ||||
|                     const id = this.getNSAttributesForElement(item, 'id'); | ||||
|                     const id = item.getAttributeNS(null, 'id'); | ||||
|                     if (id) { | ||||
|                         const timestamp = id.replace('id-', ''); | ||||
|                         const timestamp = id.replace(ID_PREFIX, ''); | ||||
|                         if (!this.isImageryInBounds({ | ||||
|                             time: timestamp | ||||
|                         })) { | ||||
| @@ -205,7 +220,7 @@ export default { | ||||
|             } | ||||
|  | ||||
|             if (timeSystem === undefined) { | ||||
|                 timeSystem = this.openmct.time.timeSystem(); | ||||
|                 timeSystem = this.timeContext.timeSystem(); | ||||
|             } | ||||
|  | ||||
|             if (timeSystem.isUTCBased) { | ||||
| @@ -223,19 +238,17 @@ export default { | ||||
|             this.xScale.range([PADDING, this.width - PADDING * 2]); | ||||
|         }, | ||||
|         isImageryInBounds(imageObj) { | ||||
|             return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start); | ||||
|             return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start); | ||||
|         }, | ||||
|         getImageryContainer() { | ||||
|             let svgHeight = 100; | ||||
|             let svgWidth = this.imageHistory.length ? this.width : 200; | ||||
|             let groupSVG; | ||||
|             let containerHeight = 100; | ||||
|             let containerWidth = this.imageHistory.length ? this.width : 200; | ||||
|             let imageryContainer; | ||||
|  | ||||
|             let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg"); | ||||
|             if (existingSVG) { | ||||
|                 groupSVG = existingSVG; | ||||
|                 this.setNSAttributesForElement(groupSVG, { | ||||
|                     width: svgWidth | ||||
|                 }); | ||||
|             let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`); | ||||
|             if (existingContainer) { | ||||
|                 imageryContainer = existingContainer; | ||||
|                 imageryContainer.style.maxWidth = `${containerWidth}px`; | ||||
|             } else { | ||||
|                 let component = new Vue({ | ||||
|                     components: { | ||||
| @@ -246,26 +259,20 @@ export default { | ||||
|                     }, | ||||
|                     data() { | ||||
|                         return { | ||||
|                             isNested: true, | ||||
|                             height: svgHeight, | ||||
|                             width: svgWidth | ||||
|                             isNested: true | ||||
|                         }; | ||||
|                     }, | ||||
|                     template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>` | ||||
|                     template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>` | ||||
|                 }); | ||||
|  | ||||
|                 this.$refs.imageryHolder.appendChild(component.$mount().$el); | ||||
|  | ||||
|                 groupSVG = component.$el.querySelector('svg'); | ||||
|  | ||||
|                 groupSVG.addEventListener('mouseout', (event) => { | ||||
|                     if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') { | ||||
|                         this.removeFromForeground(); | ||||
|                     } | ||||
|                 }); | ||||
|                 imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`); | ||||
|                 imageryContainer.style.maxWidth = `${containerWidth}px`; | ||||
|                 imageryContainer.style.height = `${containerHeight}px`; | ||||
|             } | ||||
|  | ||||
|             return groupSVG; | ||||
|             return imageryContainer; | ||||
|         }, | ||||
|         isImageryWidthAcceptable() { | ||||
|             // We're calculating if there is enough space between images to show the thumbnails. | ||||
| @@ -281,194 +288,123 @@ export default { | ||||
|             return imageContainerWidth < IMAGE_WIDTH_THRESHOLD; | ||||
|         }, | ||||
|         drawImagery() { | ||||
|             let groupSVG = this.getImageryContainer(); | ||||
|             let imageryContainer = this.getImageryContainer(); | ||||
|             const showImagePlaceholders = this.isImageryWidthAcceptable(); | ||||
|  | ||||
|             let index = 0; | ||||
|             if (this.imageHistory.length) { | ||||
|                 this.imageHistory.forEach((currentImageObject, index) => { | ||||
|                 this.imageHistory.forEach((currentImageObject) => { | ||||
|                     if (this.isImageryInBounds(currentImageObject)) { | ||||
|                         this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index); | ||||
|                         this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index); | ||||
|                         index = index + 1; | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.plotNoItems(groupSVG); | ||||
|                 this.plotNoItems(imageryContainer); | ||||
|             } | ||||
|         }, | ||||
|         plotNoItems(svgElement) { | ||||
|             let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | ||||
|             this.setNSAttributesForElement(textElement, { | ||||
|                 x: "10", | ||||
|                 y: "20", | ||||
|                 class: "c-imagery-tsv__no-items" | ||||
|             }); | ||||
|         plotNoItems(containerElement) { | ||||
|             let textElement = document.createElement('text'); | ||||
|             textElement.classList.add(NO_ITEMS_CLASS); | ||||
|             textElement.innerHTML = 'No images within timeframe'; | ||||
|  | ||||
|             svgElement.appendChild(textElement); | ||||
|             containerElement.appendChild(textElement); | ||||
|         }, | ||||
|         setNSAttributesForElement(element, attributes) { | ||||
|             Object.keys(attributes).forEach((key) => { | ||||
|                 if (key === 'url') { | ||||
|                     element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]); | ||||
|                 } else { | ||||
|                     element.setAttributeNS(null, key, attributes[key]); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         getNSAttributesForElement(element, attribute) { | ||||
|             return element.getAttributeNS(null, attribute); | ||||
|         }, | ||||
|         getImageWrapper(item) { | ||||
|             const id = `id-${item.time}`; | ||||
|  | ||||
|             return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`); | ||||
|         }, | ||||
|         plotImagery(item, showImagePlaceholders, svgElement, index) { | ||||
|             //TODO: Placeholder image | ||||
|             let existingImageWrapper = this.getImageWrapper(item); | ||||
|             //imageWrapper wraps the vertical tick rect and the image | ||||
|             if (existingImageWrapper) { | ||||
|                 this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders); | ||||
|             } else { | ||||
|                 let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement); | ||||
|                 svgElement.appendChild(imageWrapper); | ||||
|             } | ||||
|         }, | ||||
|         updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) { | ||||
|             //Update the x co-ordinates of the handle and image elements and the url of image | ||||
|             //this is to avoid tearing down all elements completely and re-drawing them | ||||
|             this.setNSAttributesForElement(existingImageWrapper, { | ||||
|                 showImagePlaceholders | ||||
|             }); | ||||
|             let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle'); | ||||
|             this.setNSAttributesForElement(imageTickElement, { | ||||
|                 x: this.xScale(item.time) | ||||
|             }); | ||||
|  | ||||
|             let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder'); | ||||
|             this.setNSAttributesForElement(imageRect, { | ||||
|                 x: this.xScale(item.time) + 2 | ||||
|             }); | ||||
|  | ||||
|             let imageElement = existingImageWrapper.querySelector('image'); | ||||
|             const selector = `href*=${existingImageWrapper.id}`; | ||||
|             let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`); | ||||
|             const hideImageUrl = (showImagePlaceholders && !hoverEl); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 url: hideImageUrl ? '' : item.url | ||||
|             }); | ||||
|         }, | ||||
|         createImageWrapper(index, item, showImagePlaceholders, svgElement) { | ||||
|             const id = `id-${item.time}`; | ||||
|             const imgSize = String(ROW_HEIGHT - 15); | ||||
|             let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g'); | ||||
|             this.setNSAttributesForElement(imageWrapper, { | ||||
|                 id, | ||||
|                 class: 'c-imagery-tsv__image-wrapper', | ||||
|                 showImagePlaceholders | ||||
|             }); | ||||
|             //create image tick indicator | ||||
|             let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | ||||
|             this.setNSAttributesForElement(imageTickElement, { | ||||
|                 class: 'c-imagery-tsv__image-handle', | ||||
|                 x: this.xScale(item.time), | ||||
|                 y: 5, | ||||
|                 rx: 0, | ||||
|                 width: 2, | ||||
|                 height: String(ROW_HEIGHT - 10) | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageTickElement); | ||||
|  | ||||
|             let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | ||||
|             this.setNSAttributesForElement(imageRect, { | ||||
|                 class: 'c-imagery-tsv__image-placeholder', | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 y: 10, | ||||
|                 rx: 0, | ||||
|                 width: imgSize, | ||||
|                 height: imgSize, | ||||
|                 mask: `#image-${item.time}` | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageRect); | ||||
|  | ||||
|             let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image'); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 id: `image-${item.time}`, | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 y: 10, | ||||
|                 rx: 0, | ||||
|                 width: imgSize, | ||||
|                 height: imgSize, | ||||
|                 url: showImagePlaceholders ? '' : item.url | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageElement); | ||||
|  | ||||
|             //TODO: Don't add the hover listener if the width is too small | ||||
|             imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url)); | ||||
|  | ||||
|             return imageWrapper; | ||||
|         }, | ||||
|         bringToForeground(svgElement, imageWrapper, index, url, event) { | ||||
|             const selector = `href*=${imageWrapper.id}`; | ||||
|             let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`); | ||||
|             if (hoverEls.length > 0) { | ||||
|                 this.removeFromForeground(hoverEls); | ||||
|             } | ||||
|  | ||||
|             hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`); | ||||
|             if (hoverEls.length) { | ||||
|  | ||||
|             if (!element) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let imageElement = imageWrapper.querySelector('image'); | ||||
|             Object.keys(attributes).forEach((key) => { | ||||
|                 element.setAttributeNS(null, key, attributes[key]); | ||||
|             }); | ||||
|         }, | ||||
|         setStyles(element, styles) { | ||||
|             if (!element) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Object.keys(styles).forEach((key) => { | ||||
|                 element.style[key] = styles[key]; | ||||
|             }); | ||||
|         }, | ||||
|         getImageWrapper(item) { | ||||
|             const id = `${ID_PREFIX}${item.time}`; | ||||
|  | ||||
|             return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`); | ||||
|         }, | ||||
|         plotImagery(item, showImagePlaceholders, containerElement, index) { | ||||
|             let existingImageWrapper = this.getImageWrapper(item); | ||||
|             //imageWrapper wraps the vertical tick and the image | ||||
|             if (existingImageWrapper) { | ||||
|                 this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders); | ||||
|             } else { | ||||
|                 let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders); | ||||
|                 containerElement.appendChild(imageWrapper); | ||||
|             } | ||||
|         }, | ||||
|         setImageDisplay(imageElement, showImagePlaceholders) { | ||||
|             if (showImagePlaceholders) { | ||||
|                 imageElement.style.display = 'none'; | ||||
|             } else { | ||||
|                 imageElement.style.display = 'block'; | ||||
|             } | ||||
|         }, | ||||
|         updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) { | ||||
|             //Update the x co-ordinates of the image wrapper and the url of image | ||||
|             //this is to avoid tearing down all elements completely and re-drawing them | ||||
|             this.setNSAttributesForElement(existingImageWrapper, { | ||||
|                 'data-show-image-placeholders': showImagePlaceholders | ||||
|             }); | ||||
|             existingImageWrapper.style.left = `${this.xScale(item.time)}px`; | ||||
|  | ||||
|             let imageElement = existingImageWrapper.querySelector('img'); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 url: url, | ||||
|                 fill: 'none' | ||||
|             }); | ||||
|             let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use'); | ||||
|             this.setNSAttributesForElement(hoverElement, { | ||||
|                 class: 'image-highlight', | ||||
|                 x: 0, | ||||
|                 href: `#${imageWrapper.id}` | ||||
|                 src: item.url | ||||
|             }); | ||||
|             this.setImageDisplay(imageElement, showImagePlaceholders); | ||||
|         }, | ||||
|         createImageWrapper(index, item, showImagePlaceholders) { | ||||
|             const id = `${ID_PREFIX}${item.time}`; | ||||
|             let imageWrapper = document.createElement('div'); | ||||
|             imageWrapper.classList.add(IMAGE_WRAPPER_CLASS); | ||||
|             imageWrapper.style.left = `${this.xScale(item.time)}px`; | ||||
|             this.setNSAttributesForElement(imageWrapper, { | ||||
|                 class: 'c-imagery-tsv__image-wrapper is-hovered' | ||||
|                 id, | ||||
|                 'data-show-image-placeholders': showImagePlaceholders | ||||
|             }); | ||||
|             // We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably | ||||
|             hoverElement.addEventListener('mousedown', (e) => { | ||||
|             //create image vertical tick indicator | ||||
|             let imageTickElement = document.createElement('div'); | ||||
|             imageTickElement.classList.add('c-imagery-tsv__image-handle'); | ||||
|             imageTickElement.style.width = '2px'; | ||||
|             imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`; | ||||
|             imageWrapper.appendChild(imageTickElement); | ||||
|  | ||||
|             //create placeholder - this will also hold the actual image | ||||
|             let imagePlaceholder = document.createElement('div'); | ||||
|             imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder'); | ||||
|             imagePlaceholder.style.width = `${IMAGE_SIZE}px`; | ||||
|             imagePlaceholder.style.height = `${IMAGE_SIZE}px`; | ||||
|             imageWrapper.appendChild(imagePlaceholder); | ||||
|  | ||||
|             //create image element | ||||
|             let imageElement = document.createElement('img'); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 src: item.url | ||||
|             }); | ||||
|             imageElement.style.width = `${IMAGE_SIZE}px`; | ||||
|             imageElement.style.height = `${IMAGE_SIZE}px`; | ||||
|             this.setImageDisplay(imageElement, showImagePlaceholders); | ||||
|  | ||||
|             //handle mousedown event to show the image in a large view | ||||
|             imageWrapper.addEventListener('mousedown', (e) => { | ||||
|                 if (e.button === 0) { | ||||
|                     this.expand(index); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             svgElement.appendChild(hoverElement); | ||||
|             imagePlaceholder.appendChild(imageElement); | ||||
|  | ||||
|         }, | ||||
|         removeFromForeground(items) { | ||||
|             let hoverEls; | ||||
|             if (items) { | ||||
|                 hoverEls = items; | ||||
|             } else { | ||||
|                 hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use"); | ||||
|             } | ||||
|  | ||||
|             hoverEls.forEach(item => { | ||||
|                 let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`; | ||||
|                 let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`); | ||||
|                 this.setNSAttributesForElement(imageWrapper, { | ||||
|                     class: 'c-imagery-tsv__image-wrapper' | ||||
|                 }); | ||||
|                 let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders'); | ||||
|                 if (showImagePlaceholders === 'true') { | ||||
|                     let imageElement = imageWrapper.querySelector('image'); | ||||
|                     this.setNSAttributesForElement(imageElement, { | ||||
|                         url: '' | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 item.remove(); | ||||
|             }); | ||||
|             return imageWrapper; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -57,7 +57,7 @@ | ||||
|         </div> | ||||
|         <div ref="imageBG" | ||||
|              class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|              :class="{'paused unnsynced': isPaused && !isFixed,'stale':false }" | ||||
|              @click="expand" | ||||
|         > | ||||
|             <div class="image-wrapper" | ||||
| @@ -122,6 +122,7 @@ | ||||
|             </div> | ||||
|             <div class="h-local-controls"> | ||||
|                 <button | ||||
|                     v-if="!isFixed" | ||||
|                     class="c-button icon-pause pause-play" | ||||
|                     :class="{'is-paused': isPaused}" | ||||
|                     @click="paused(!isPaused, 'button')" | ||||
| @@ -131,7 +132,7 @@ | ||||
|     </div> | ||||
|     <div class="c-imagery__thumbs-wrapper" | ||||
|          :class="[ | ||||
|              { 'is-paused': isPaused }, | ||||
|              { 'is-paused': isPaused && !isFixed }, | ||||
|              { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } | ||||
|          ]" | ||||
|     > | ||||
| @@ -199,6 +200,14 @@ export default { | ||||
|     }, | ||||
|     mixins: [imageryData], | ||||
|     inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], | ||||
|     props: { | ||||
|         indexForFocusedImage: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         this.metadata = {}; | ||||
| @@ -226,7 +235,8 @@ export default { | ||||
|             imageContainerWidth: undefined, | ||||
|             imageContainerHeight: undefined, | ||||
|             lockCompass: true, | ||||
|             resizingWindow: false | ||||
|             resizingWindow: false, | ||||
|             timeContext: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -258,7 +268,14 @@ export default { | ||||
|             return age < cutoff && !this.refreshCSS; | ||||
|         }, | ||||
|         canTrackDuration() { | ||||
|             return this.openmct.time.clock() && this.timeSystem.isUTCBased; | ||||
|             let hasClock; | ||||
|             if (this.timeContext) { | ||||
|                 hasClock = this.timeContext.clock(); | ||||
|             } else { | ||||
|                 hasClock = this.openmct.time.clock(); | ||||
|             } | ||||
|  | ||||
|             return hasClock && this.timeSystem.isUTCBased; | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
| @@ -379,11 +396,28 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return sizedImageDimensions; | ||||
|         }, | ||||
|         isFixed() { | ||||
|             let clock; | ||||
|             if (this.timeContext) { | ||||
|                 clock = this.timeContext.clock(); | ||||
|             } else { | ||||
|                 clock = this.openmct.time.clock(); | ||||
|             } | ||||
|  | ||||
|             return clock === undefined; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         imageHistorySize(newSize, oldSize) { | ||||
|             this.setFocusedImage(newSize - 1, false); | ||||
|             let imageIndex; | ||||
|             if (this.indexForFocusedImage !== undefined) { | ||||
|                 imageIndex = this.initFocusedImageIndex; | ||||
|             } else { | ||||
|                 imageIndex = newSize - 1; | ||||
|             } | ||||
|  | ||||
|             this.setFocusedImage(imageIndex, false); | ||||
|             this.scrollToRight(); | ||||
|         }, | ||||
|         focusedImageIndex() { | ||||
| @@ -394,9 +428,14 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     async mounted() { | ||||
|         //listen | ||||
|         this.openmct.time.on('timeSystem', this.trackDuration); | ||||
|         this.openmct.time.on('clock', this.trackDuration); | ||||
|         //We only need to use this till the user focuses an image manually | ||||
|         if (this.indexForFocusedImage !== undefined) { | ||||
|             this.initFocusedImageIndex = this.indexForFocusedImage; | ||||
|             this.isPaused = true; | ||||
|         } | ||||
|  | ||||
|         this.setTimeContext = this.setTimeContext.bind(this); | ||||
|         this.setTimeContext(); | ||||
|  | ||||
|         // related telemetry keys | ||||
|         this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; | ||||
| @@ -432,8 +471,7 @@ export default { | ||||
|  | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.time.off('timeSystem', this.trackDuration); | ||||
|         this.openmct.time.off('clock', this.trackDuration); | ||||
|         this.stopFollowingTimeContext(); | ||||
|  | ||||
|         if (this.thumbWrapperResizeObserver) { | ||||
|             this.thumbWrapperResizeObserver.disconnect(); | ||||
| @@ -457,6 +495,21 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
|             //listen | ||||
|             this.timeContext.on('timeSystem', this.trackDuration); | ||||
|             this.timeContext.on('clock', this.trackDuration); | ||||
|             this.timeContext.on("timeContext", this.setTimeContext); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off("timeSystem", this.trackDuration); | ||||
|                 this.timeContext.off("clock", this.trackDuration); | ||||
|                 this.timeContext.off("timeContext", this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         expand() { | ||||
|             const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); | ||||
|             const visibleActions = actionCollection.getVisibleActions(); | ||||
| @@ -618,7 +671,12 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             if (this.isPaused && !thumbnailClick) { | ||||
|             if (thumbnailClick) { | ||||
|                 //We use the props till the user changes what they want to see | ||||
|                 this.initFocusedImageIndex = undefined; | ||||
|             } | ||||
|  | ||||
|             if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) { | ||||
|                 this.nextImageIndex = index; | ||||
|                 //this could happen if bounds changes | ||||
|                 if (this.focusedImageIndex > this.imageHistory.length - 1) { | ||||
| @@ -649,8 +707,12 @@ export default { | ||||
|             window.clearInterval(this.durationTracker); | ||||
|         }, | ||||
|         updateDuration() { | ||||
|             let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue(); | ||||
|             this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|             let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue(); | ||||
|             if (currentTime === undefined) { | ||||
|                 this.numericDuration = currentTime; | ||||
|             } else { | ||||
|                 this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|             } | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
|             this.refreshCSS = true; | ||||
|   | ||||
| @@ -315,13 +315,31 @@ | ||||
|  | ||||
| /*************************************** IMAGERY IN TIMESTRIP VIEWS */ | ||||
| .c-imagery-tsv { | ||||
|     g.c-imagery-tsv__image-wrapper { | ||||
|     div.c-imagery-tsv__image-wrapper { | ||||
|         cursor: pointer; | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         display: flex; | ||||
|         z-index: 1; | ||||
|         margin-top: 5px; | ||||
|  | ||||
|         img { | ||||
|             align-self: flex-end; | ||||
|         } | ||||
|         &:hover { | ||||
|             z-index: 2; | ||||
|  | ||||
|         &.is-hovered { | ||||
|             filter: brightness(1) contrast(1) !important; | ||||
|             [class*='__image-handle'] { | ||||
|                 fill: $colorBodyFg; | ||||
|                 background-color: $colorBodyFg; | ||||
|             } | ||||
|  | ||||
|             //[class*='__image-placeholder'] { | ||||
|             //    display: none; | ||||
|             //} | ||||
|  | ||||
|             img { | ||||
|                 display: block !important; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -331,14 +349,16 @@ | ||||
|     } | ||||
|  | ||||
|     &__image-handle { | ||||
|         fill: rgba($colorBodyFg, 0.5); | ||||
|         background-color: rgba($colorBodyFg, 0.5); | ||||
|     } | ||||
|  | ||||
|     &__image-placeholder { | ||||
|         fill: pushBack($colorBodyBg, 0.3); | ||||
|         background-color: pushBack($colorBodyBg, 0.3); | ||||
|         display: block; | ||||
|         align-self: flex-end; | ||||
|     } | ||||
|  | ||||
|     &:hover g.c-imagery-tsv__image-wrapper { | ||||
|     &:hover div.c-imagery-tsv__image-wrapper { | ||||
|         // TODO CH: convert to theme constants | ||||
|         filter: brightness(0.5) contrast(0.7); | ||||
|     } | ||||
|   | ||||
| @@ -26,8 +26,10 @@ export default { | ||||
|     inject: ['openmct', 'domainObject', 'objectPath'], | ||||
|     mounted() { | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
|         this.boundsChange = this.boundsChange.bind(this); | ||||
|         this.timeSystemChange = this.timeSystemChange.bind(this); | ||||
|         this.setDataTimeContext = this.setDataTimeContext.bind(this); | ||||
|         this.setDataTimeContext(); | ||||
|  | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
| @@ -51,10 +53,24 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|         this.stopFollowingDataTimeContext(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setDataTimeContext() { | ||||
|             this.stopFollowingDataTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
|             this.timeContext.on('bounds', this.boundsChange); | ||||
|             this.boundsChange(this.timeContext.bounds()); | ||||
|             this.timeContext.on('timeSystem', this.timeSystemChange); | ||||
|             this.timeContext.on("timeContext", this.setDataTimeContext); | ||||
|         }, | ||||
|         stopFollowingDataTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.boundsChange); | ||||
|                 this.timeContext.off('timeSystem', this.timeSystemChange); | ||||
|                 this.timeContext.off("timeContext", this.setDataTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
| @@ -111,7 +127,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             let bounds = this.timeContext.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
| @@ -132,7 +148,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         timeSystemChange() { | ||||
|             this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeSystem = this.timeContext.timeSystem(); | ||||
|             this.timeKey = this.timeSystem.key; | ||||
|             this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|             this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
| @@ -141,7 +157,7 @@ export default { | ||||
|             this.unsubscribe = this.openmct.telemetry | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
|                     let bounds = this.timeContext.bounds(); | ||||
|  | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         let image = this.normalizeDatum(datum); | ||||
| @@ -159,7 +175,7 @@ export default { | ||||
|             let image = { ...datum }; | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = datum[this.timeKey]; | ||||
|             image.time = this.parseTime(image.formattedTime); | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
|  | ||||
|             return image; | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| import { | ||||
|     createMouseEvent, | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     simulateKeyEvent | ||||
| @@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10; | ||||
| const MAIN_IMAGE_CLASS = '.js-imageryView-image'; | ||||
| const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| // const TOLERANCE = 0.50; | ||||
|  | ||||
| // function comparisonFunction(valueOne, valueTwo) { | ||||
| //     let larger = valueOne; | ||||
| //     let smaller = valueTwo; | ||||
| // | ||||
| //     if (larger < smaller) { | ||||
| //         larger = valueTwo; | ||||
| //         smaller = valueOne; | ||||
| //     } | ||||
| // | ||||
| //     return (larger - smaller) < TOLERANCE; | ||||
| // } | ||||
|  | ||||
| function getImageInfo(doc) { | ||||
|     let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; | ||||
| @@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => { | ||||
|     const START = Date.now(); | ||||
|     const COUNT = 10; | ||||
|  | ||||
|     let resolveFunction; | ||||
|     // let resolveFunction; | ||||
|     let originalRouterPath; | ||||
|     let telemetryPromise; | ||||
|     let telemetryPromiseResolve; | ||||
|     let cleanupFirst; | ||||
|  | ||||
|     let openmct; | ||||
|     let appHolder; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); | ||||
| @@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => { | ||||
|  | ||||
|     // this setups up the app | ||||
|     beforeEach((done) => { | ||||
|         appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|         cleanupFirst = []; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: START - (5 * ONE_MINUTE), | ||||
|             end: START + (5 * ONE_MINUTE) | ||||
|         }); | ||||
|  | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         telemetryPromise = new Promise((resolve) => { | ||||
|             telemetryPromiseResolve = resolve; | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|             telemetryPromiseResolve(imageTelemetry); | ||||
|  | ||||
|             return telemetryPromise; | ||||
|         }); | ||||
|  | ||||
|         parent = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
|         parent.style.width = '640px'; | ||||
|         parent.style.height = '480px'; | ||||
|  | ||||
|         // document.querySelector('body').append(parent); | ||||
|         child = document.createElement('div'); | ||||
|         child.style.width = '640px'; | ||||
|         child.style.height = '480px'; | ||||
|  | ||||
|         parent.appendChild(child); | ||||
|         document.body.appendChild(parent); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|         // spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|  | ||||
|         originalRouterPath = openmct.router.path; | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|     afterEach((done) => { | ||||
|         openmct.router.path = originalRouterPath; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|         // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after | ||||
|         // teardown, which causes problems | ||||
|         // This is hacky, we should find a better approach here. | ||||
|         setTimeout(() => { | ||||
|             //Cleanup code that needs to happen before dom elements start being destroyed | ||||
|             cleanupFirst.forEach(cleanup => cleanup()); | ||||
|             cleanupFirst = []; | ||||
|             document.body.removeChild(parent); | ||||
|  | ||||
|             resetApplicationState(openmct).then(done).catch(done); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery time strip view when in a time strip", () => { | ||||
| @@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => { | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery view only for imagery producing objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryKey | ||||
|         ); | ||||
| @@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => { | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryRequestPromise = new Promise((resolve) => { | ||||
|                 telemetryRequestResolve = resolve; | ||||
|             }); | ||||
|         beforeEach(() => { | ||||
|  | ||||
|             openmct.telemetry.request.and.callFake(() => { | ||||
|                 telemetryRequestResolve(imageTelemetry); | ||||
|  | ||||
|                 return telemetryRequestPromise; | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject); | ||||
|             imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             await telemetryRequestPromise; | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             openmct.time.stopClock(); | ||||
|             openmct.router.removeListener('change:hash', resolveFunction); | ||||
|         // afterEach(() => { | ||||
|         //     openmct.time.stopClock(); | ||||
|         //     openmct.router.removeListener('change:hash', resolveFunction); | ||||
|         // | ||||
|         //     imageryView.destroy(); | ||||
|         // }); | ||||
|  | ||||
|             imageryView.destroy(); | ||||
|         }); | ||||
|  | ||||
|         it("on mount should show the the most recent image", () => { | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         xit("should show the clicked thumbnail as the main image", (done) => { | ||||
|             const target = imageTelemetry[5].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|         it("on mount should show the the most recent image", (done) => { | ||||
|             //Looks like we need Vue.nextTick here so that computed properties settle down | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should show the clicked thumbnail as the main image", (done) => { | ||||
|             //Looks like we need Vue.nextTick here so that computed properties settle down | ||||
|             Vue.nextTick(() => { | ||||
|                 const target = imageTelemetry[5].url; | ||||
|                 parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|                 Vue.nextTick(() => { | ||||
|                     const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|                     expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         xit("should show that an image is new", (done) => { | ||||
|             openmct.time.clock('local', { | ||||
|                 start: -1000, | ||||
|                 end: 1000 | ||||
|             }); | ||||
|  | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
| @@ -385,69 +396,148 @@ describe("The Imagery View Layouts", () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         xit("should navigate via arrow keys", (done) => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
|                 keyCode: 37, | ||||
|                 type: 'keyup' | ||||
|             }; | ||||
|  | ||||
|             simulateKeyEvent(keyOpts); | ||||
|  | ||||
|         it("should navigate via arrow keys", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|                 let keyOpts = { | ||||
|                     element: parent.querySelector('.c-imagery'), | ||||
|                     key: 'ArrowLeft', | ||||
|                     keyCode: 37, | ||||
|                     type: 'keyup' | ||||
|                 }; | ||||
|  | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|                 simulateKeyEvent(keyOpts); | ||||
|  | ||||
|                 Vue.nextTick(() => { | ||||
|                     const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|                     expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via numerous arrow keys", (done) => { | ||||
|             let element = parent.querySelector('.c-imagery'); | ||||
|             let type = 'keyup'; | ||||
|             let leftKeyOpts = { | ||||
|                 element, | ||||
|                 type, | ||||
|                 key: 'ArrowLeft', | ||||
|                 keyCode: 37 | ||||
|             }; | ||||
|             let rightKeyOpts = { | ||||
|                 element, | ||||
|                 type, | ||||
|                 key: 'ArrowRight', | ||||
|                 keyCode: 39 | ||||
|             }; | ||||
|  | ||||
|             // left thrice | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             // right once | ||||
|             simulateKeyEvent(rightKeyOpts); | ||||
|  | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|                 let element = parent.querySelector('.c-imagery'); | ||||
|                 let type = 'keyup'; | ||||
|                 let leftKeyOpts = { | ||||
|                     element, | ||||
|                     type, | ||||
|                     key: 'ArrowLeft', | ||||
|                     keyCode: 37 | ||||
|                 }; | ||||
|                 let rightKeyOpts = { | ||||
|                     element, | ||||
|                     type, | ||||
|                     key: 'ArrowRight', | ||||
|                     keyCode: 39 | ||||
|                 }; | ||||
|  | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                 // left thrice | ||||
|                 simulateKeyEvent(leftKeyOpts); | ||||
|                 simulateKeyEvent(leftKeyOpts); | ||||
|                 simulateKeyEvent(leftKeyOpts); | ||||
|                 // right once | ||||
|                 simulateKeyEvent(rightKeyOpts); | ||||
|  | ||||
|                 Vue.nextTick(() => { | ||||
|                     const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|                     expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|         it ('shows an auto scroll button when scroll to left', (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 // to mock what a scroll would do | ||||
|                 imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|                 Vue.nextTick(() => { | ||||
|                     let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button'); | ||||
|                     expect(autoScrollButton).toBeTruthy(); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|         it ('scrollToRight is called when clicking on auto scroll button', (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 // use spyon to spy the scroll function | ||||
|                 spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight'); | ||||
|                 imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|                 Vue.nextTick(() => { | ||||
|                     parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); | ||||
|                     expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset'); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("imagery time strip view", () => { | ||||
|         let applicableViews; | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|         let componentView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: START - (5 * ONE_MINUTE), | ||||
|                 end: START + (5 * ONE_MINUTE) | ||||
|             }); | ||||
|  | ||||
|             openmct.router.path = [{ | ||||
|                 identifier: { | ||||
|                     key: 'test-timestrip', | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 type: 'time-strip' | ||||
|             }]; | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { | ||||
|                 identifier: { | ||||
|                     key: 'test-timestrip', | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 type: 'time-strip' | ||||
|             }]); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject, [imageryObject, { | ||||
|                 identifier: { | ||||
|                     key: 'test-timestrip', | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 type: 'time-strip' | ||||
|             }]); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             componentView = imageryView.getComponent().$children[0]; | ||||
|             spyOn(componentView.previewAction, 'invoke').and.callThrough(); | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         it("on mount should show imagery within the given bounds", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper'); | ||||
|                 expect(imageElements.length).toEqual(6); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         it ('shows an auto scroll button when scroll to left', async () => { | ||||
|             // to mock what a scroll would do | ||||
|             imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|             await Vue.nextTick(); | ||||
|             let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button'); | ||||
|             expect(autoScrollButton).toBeTruthy(); | ||||
|         }); | ||||
|         it ('scrollToRight is called when clicking on auto scroll button', async () => { | ||||
|             // use spyon to spy the scroll function | ||||
|             spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight'); | ||||
|             imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|             await Vue.nextTick(); | ||||
|             parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); | ||||
|             expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset'); | ||||
|  | ||||
|         it("should show the clicked thumbnail as the preview image", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 const mouseDownEvent = createMouseEvent("mousedown"); | ||||
|                 let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`); | ||||
|                 imageWrapper[2].dispatchEvent(mouseDownEvent); | ||||
|  | ||||
|                 Vue.nextTick(() => { | ||||
|                     expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], { | ||||
|                         indexForFocusedImage: 2, | ||||
|                         objectPath: componentView.objectPath | ||||
|                     }); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -180,9 +180,13 @@ export default { | ||||
|                 this.openmct.notifications.alert(message); | ||||
|             } | ||||
|  | ||||
|             const relativeHash = hash.slice(hash.indexOf('#')); | ||||
|             const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); | ||||
|             this.openmct.router.navigate(url.hash); | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 this.previewEmbed(); | ||||
|             } else { | ||||
|                 const relativeHash = hash.slice(hash.indexOf('#')); | ||||
|                 const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); | ||||
|                 this.openmct.router.navigate(url.hash); | ||||
|             } | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|   | ||||
							
								
								
									
										72
									
								
								src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| import {NOTEBOOK_TYPE} from './notebook-constants'; | ||||
|  | ||||
| export default function (openmct) { | ||||
|     const apiSave = openmct.objects.save.bind(openmct.objects); | ||||
|  | ||||
|     openmct.objects.save = async (domainObject) => { | ||||
|         if (domainObject.type !== NOTEBOOK_TYPE) { | ||||
|             return apiSave(domainObject); | ||||
|         } | ||||
|  | ||||
|         const localMutable = openmct.objects._toMutable(domainObject); | ||||
|         let result; | ||||
|  | ||||
|         try { | ||||
|             result = await apiSave(localMutable); | ||||
|         } catch (error) { | ||||
|             if (error instanceof openmct.objects.errors.Conflict) { | ||||
|                 result = resolveConflicts(localMutable, openmct); | ||||
|             } else { | ||||
|                 result = Promise.reject(error); | ||||
|             } | ||||
|         } finally { | ||||
|             openmct.objects.destroyMutable(localMutable); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function resolveConflicts(localMutable, openmct) { | ||||
|     return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => { | ||||
|         const localEntries = localMutable.configuration.entries; | ||||
|         remoteMutable.$refresh(remoteMutable); | ||||
|         applyLocalEntries(remoteMutable, localEntries); | ||||
|  | ||||
|         openmct.objects.destroyMutable(remoteMutable); | ||||
|  | ||||
|         return true; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function applyLocalEntries(mutable, entries) { | ||||
|     Object.entries(entries).forEach(([sectionKey, pagesInSection]) => { | ||||
|         Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => { | ||||
|             const remoteEntries = mutable.configuration.entries[sectionKey][pageKey]; | ||||
|             const mergedEntries = [].concat(remoteEntries); | ||||
|             let shouldMutate = false; | ||||
|  | ||||
|             const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id'); | ||||
|             const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => { | ||||
|                 return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text; | ||||
|             }); | ||||
|  | ||||
|             locallyAddedEntries.forEach((localEntry) => { | ||||
|                 mergedEntries.push(localEntry); | ||||
|                 shouldMutate = true; | ||||
|             }); | ||||
|  | ||||
|             locallyModifiedEntries.forEach((locallyModifiedEntry) => { | ||||
|                 let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id); | ||||
|                 if (mergedEntry !== undefined) { | ||||
|                     mergedEntry.text = locallyModifiedEntry.text; | ||||
|                     shouldMutate = true; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (shouldMutate) { | ||||
|                 mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction'; | ||||
| import Notebook from './components/Notebook.vue'; | ||||
| import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js'; | ||||
|  | ||||
| import { notebookImageMigration } from '../notebook/utils/notebook-migration'; | ||||
| import { NOTEBOOK_TYPE } from './notebook-constants'; | ||||
| @@ -165,5 +166,7 @@ export default function NotebookPlugin() { | ||||
|                 return domainObject; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         monkeyPatchObjectAPIForNotebooks(openmct); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -154,6 +154,8 @@ describe("Notebook plugin:", () => { | ||||
|             testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); | ||||
|             openmct.objects.addProvider('test-namespace', testObjectProvider); | ||||
|             testObjectProvider.observe.and.returnValue(() => {}); | ||||
|             testObjectProvider.create.and.returnValue(Promise.resolve(true)); | ||||
|             testObjectProvider.update.and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|             return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => { | ||||
|                 mutableNotebookObject = mutableObject; | ||||
|   | ||||
| @@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = | ||||
|     const newEntries = addEntryIntoPage(notebookStorage, entries, entry); | ||||
|  | ||||
|     addDefaultClass(domainObject, openmct); | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', newEntries); | ||||
|     domainObject.configuration.entries = newEntries; | ||||
|  | ||||
|     return id; | ||||
| } | ||||
|   | ||||
| @@ -15,12 +15,16 @@ | ||||
|  | ||||
|         port.onmessage = async function (event) { | ||||
|             if (event.data.request === 'close') { | ||||
|                 console.log('Closing connection'); | ||||
|                 connections.splice(event.data.connectionId - 1, 1); | ||||
|                 if (connections.length <= 0) { | ||||
|                     // abort any outstanding requests if there's nobody listening to it. | ||||
|                     controller.abort(); | ||||
|                 } | ||||
|  | ||||
|                 console.log('Closed.'); | ||||
|                 connected = false; | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -29,68 +33,9 @@ | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 connected = true; | ||||
|  | ||||
|                 let url = event.data.url; | ||||
|                 let body = event.data.body; | ||||
|                 let error = false; | ||||
|                 // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection | ||||
|                 // style=main_only returns only the current winning revision of the document | ||||
|  | ||||
|                 const response = await fetch(url, { | ||||
|                     method: 'POST', | ||||
|                     headers: { | ||||
|                         "Content-Type": 'application/json' | ||||
|                     }, | ||||
|                     signal, | ||||
|                     body | ||||
|                 }); | ||||
|  | ||||
|                 let reader; | ||||
|  | ||||
|                 if (response.body === undefined) { | ||||
|                     error = true; | ||||
|                 } else { | ||||
|                     reader = response.body.getReader(); | ||||
|                 } | ||||
|  | ||||
|                 while (!error) { | ||||
|                     const {done, value} = await reader.read(); | ||||
|                     //done is true when we lose connection with the provider | ||||
|                     if (done) { | ||||
|                         error = true; | ||||
|                     } | ||||
|  | ||||
|                     if (value) { | ||||
|                         let chunk = new Uint8Array(value.length); | ||||
|                         chunk.set(value, 0); | ||||
|                         const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n'); | ||||
|                         if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') { | ||||
|                             decodedChunk.forEach((doc, index) => { | ||||
|                                 try { | ||||
|                                     if (doc) { | ||||
|                                         const objectChanges = JSON.parse(doc); | ||||
|                                         connections.forEach(function (connection) { | ||||
|                                             connection.postMessage({ | ||||
|                                                 objectChanges | ||||
|                                             }); | ||||
|                                         }); | ||||
|                                     } | ||||
|                                 } catch (decodeError) { | ||||
|                                     //do nothing; | ||||
|                                     console.log(decodeError); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|                 if (error) { | ||||
|                     port.postMessage({ | ||||
|                         error | ||||
|                     }); | ||||
|                 } | ||||
|                 do { | ||||
|                     await self.listenForChanges(event.data.url, event.data.body, port); | ||||
|                 } while (connected); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -103,4 +48,64 @@ | ||||
|         console.log('Error on feed'); | ||||
|     }; | ||||
|  | ||||
|     self.listenForChanges = async function (url, body, port) { | ||||
|         connected = true; | ||||
|         let error = false; | ||||
|         // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection | ||||
|         // style=main_only returns only the current winning revision of the document | ||||
|  | ||||
|         console.log('Opening changes feed connection.'); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 "Content-Type": 'application/json' | ||||
|             }, | ||||
|             signal, | ||||
|             body | ||||
|         }); | ||||
|  | ||||
|         let reader; | ||||
|  | ||||
|         if (response.body === undefined) { | ||||
|             error = true; | ||||
|         } else { | ||||
|             reader = response.body.getReader(); | ||||
|         } | ||||
|  | ||||
|         while (!error) { | ||||
|             const {done, value} = await reader.read(); | ||||
|             //done is true when we lose connection with the provider | ||||
|             if (done) { | ||||
|                 error = true; | ||||
|             } | ||||
|  | ||||
|             if (value) { | ||||
|                 let chunk = new Uint8Array(value.length); | ||||
|                 chunk.set(value, 0); | ||||
|                 const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n'); | ||||
|                 console.log('Received chunk'); | ||||
|                 if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') { | ||||
|                     decodedChunk.forEach((doc, index) => { | ||||
|                         try { | ||||
|                             if (doc) { | ||||
|                                 const objectChanges = JSON.parse(doc); | ||||
|                                 connections.forEach(function (connection) { | ||||
|                                     connection.postMessage({ | ||||
|                                         objectChanges | ||||
|                                     }); | ||||
|                                 }); | ||||
|                             } | ||||
|                         } catch (decodeError) { | ||||
|                             //do nothing; | ||||
|                             console.log(decodeError); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         console.log('Done reading changes feed'); | ||||
|     }; | ||||
|  | ||||
| }()); | ||||
|   | ||||
| @@ -29,7 +29,7 @@ const ID = "_id"; | ||||
| const HEARTBEAT = 50000; | ||||
| const ALL_DOCS = "_all_docs?include_docs=true"; | ||||
|  | ||||
| export default class CouchObjectProvider { | ||||
| class CouchObjectProvider { | ||||
|     constructor(openmct, options, namespace) { | ||||
|         options = this._normalize(options); | ||||
|         this.openmct = openmct; | ||||
| @@ -74,13 +74,6 @@ export default class CouchObjectProvider { | ||||
|         if (event.data.type === 'connection') { | ||||
|             this.changesFeedSharedWorkerConnectionId = event.data.connectionId; | ||||
|         } else { | ||||
|             const error = event.data.error; | ||||
|             if (error && Object.keys(this.observers).length > 0) { | ||||
|                 this.observeObjectChanges(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let objectChanges = event.data.objectChanges; | ||||
|             objectChanges.identifier = { | ||||
|                 namespace: this.namespace, | ||||
| @@ -126,11 +119,12 @@ export default class CouchObjectProvider { | ||||
|         } | ||||
|  | ||||
|         return fetch(this.url + '/' + subPath, fetchOptions) | ||||
|             .then(response => response.json()) | ||||
|             .then(function (response) { | ||||
|                 return response; | ||||
|             }, function () { | ||||
|                 return undefined; | ||||
|             .then((response) => { | ||||
|                 if (response.status === CouchObjectProvider.HTTP_CONFLICT) { | ||||
|                     throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`); | ||||
|                 } | ||||
|  | ||||
|                 return response.json(); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
| @@ -561,12 +555,18 @@ export default class CouchObjectProvider { | ||||
|         let intermediateResponse = this.getIntermediateResponse(); | ||||
|         const key = model.identifier.key; | ||||
|         this.enqueueObject(key, model, intermediateResponse); | ||||
|         this.objectQueue[key].pending = true; | ||||
|         const queued = this.objectQueue[key].dequeue(); | ||||
|         let document = new CouchDocument(key, queued.model); | ||||
|         this.request(key, "PUT", document).then((response) => { | ||||
|             this.checkResponse(response, queued.intermediateResponse, key); | ||||
|         }); | ||||
|         if (!this.objectQueue[key].pending) { | ||||
|             this.objectQueue[key].pending = true; | ||||
|             const queued = this.objectQueue[key].dequeue(); | ||||
|             let document = new CouchDocument(key, queued.model); | ||||
|             this.request(key, "PUT", document).then((response) => { | ||||
|                 console.log('create check response', key); | ||||
|                 this.checkResponse(response, queued.intermediateResponse, key); | ||||
|             }).catch(error => { | ||||
|                 queued.intermediateResponse.reject(error); | ||||
|                 this.objectQueue[key].pending = false; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return intermediateResponse.promise; | ||||
|     } | ||||
| @@ -581,6 +581,9 @@ export default class CouchObjectProvider { | ||||
|             let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); | ||||
|             this.request(key, "PUT", document).then((response) => { | ||||
|                 this.checkResponse(response, queued.intermediateResponse, key); | ||||
|             }).catch((error) => { | ||||
|                 queued.intermediateResponse.reject(error); | ||||
|                 this.objectQueue[key].pending = false; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| @@ -594,3 +597,7 @@ export default class CouchObjectProvider { | ||||
|         return intermediateResponse.promise; | ||||
|     } | ||||
| } | ||||
|  | ||||
| CouchObjectProvider.HTTP_CONFLICT = 409; | ||||
|  | ||||
| export default CouchObjectProvider; | ||||
|   | ||||
| @@ -25,7 +25,9 @@ import Vue from 'vue'; | ||||
|  | ||||
| export default function PlanViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip') !== undefined; | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|   | ||||
| @@ -30,6 +30,7 @@ describe('the plugin', function () { | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let appHolder; | ||||
|     let originalRouterPath; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         appHolder = document.createElement('div'); | ||||
| @@ -49,11 +50,15 @@ describe('the plugin', function () { | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         originalRouterPath = openmct.router.path; | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.router.path = originalRouterPath; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
| @@ -78,8 +83,9 @@ describe('the plugin', function () { | ||||
|                 id: "test-object", | ||||
|                 type: "plan" | ||||
|             }; | ||||
|             openmct.router.path = [testViewObject]; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); | ||||
|             let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); | ||||
|             expect(planView).toBeDefined(); | ||||
|         }); | ||||
| @@ -137,7 +143,9 @@ describe('the plugin', function () { | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(planDomainObject, []); | ||||
|             openmct.router.path = [planDomainObject]; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]); | ||||
|             planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); | ||||
|             let view = planView.view(planDomainObject, mockObjectPath); | ||||
|             view.show(child, true); | ||||
|   | ||||
| @@ -35,12 +35,15 @@ | ||||
|                 :tick-width="tickWidth" | ||||
|                 :single-series="seriesModels.length === 1" | ||||
|                 :series-model="seriesModels[0]" | ||||
|                 :style="{ | ||||
|                     left: (plotWidth - tickWidth) + 'px' | ||||
|                 }" | ||||
|                 @yKeyChanged="setYAxisKey" | ||||
|                 @tickWidthChanged="onTickWidthChange" | ||||
|         /> | ||||
|         <div class="gl-plot-wrapper-display-area-and-x-axis" | ||||
|              :style="{ | ||||
|                  left: (tickWidth + 20) + 'px' | ||||
|                  left: (plotWidth + 20) + 'px' | ||||
|              }" | ||||
|         > | ||||
|  | ||||
| @@ -219,7 +222,8 @@ export default { | ||||
|             isRealTime: this.openmct.time.clock() !== undefined, | ||||
|             loaded: false, | ||||
|             isTimeOutOfSync: false, | ||||
|             showLimitLineLabels: undefined | ||||
|             showLimitLineLabels: undefined, | ||||
|             isFrozenOnMouseDown: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -235,11 +239,9 @@ export default { | ||||
|             } else { | ||||
|                 return 'plot-legend-collapsed'; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         plotTickWidth(newTickWidth) { | ||||
|             this.onTickWidthChange(newTickWidth, true); | ||||
|         }, | ||||
|         plotWidth() { | ||||
|             return this.plotTickWidth || this.tickWidth; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -336,6 +338,11 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         loadSeriesData(series) { | ||||
|             //this check ensures that duplicate requests don't happen on load | ||||
|             if (!this.timeContext) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.$parent.$refs.plotWrapper.offsetWidth === 0) { | ||||
|                 this.scheduleLoad(series); | ||||
|  | ||||
| @@ -345,9 +352,12 @@ export default { | ||||
|             this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; | ||||
|  | ||||
|             this.startLoading(); | ||||
|             const bounds = this.timeContext.bounds(); | ||||
|             const options = { | ||||
|                 size: this.$parent.$refs.plotWrapper.offsetWidth, | ||||
|                 domain: this.config.xAxis.get('key') | ||||
|                 domain: this.config.xAxis.get('key'), | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end | ||||
|             }; | ||||
|  | ||||
|             series.load(options) | ||||
| @@ -356,9 +366,10 @@ export default { | ||||
|  | ||||
|         loadMoreData(range, purge) { | ||||
|             this.config.series.forEach(plotSeries => { | ||||
|                 this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; | ||||
|                 this.startLoading(); | ||||
|                 plotSeries.load({ | ||||
|                     size: this.$parent.$refs.plotWrapper.offsetWidth, | ||||
|                     size: this.offsetWidth, | ||||
|                     start: range.min, | ||||
|                     end: range.max, | ||||
|                     domain: this.config.xAxis.get('key') | ||||
| @@ -593,7 +604,8 @@ export default { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.$emit('plotTickWidth', this.tickWidth); | ||||
|             const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             this.$emit('plotTickWidth', this.tickWidth, id); | ||||
|         }, | ||||
|  | ||||
|         trackMousePosition(event) { | ||||
| @@ -686,6 +698,11 @@ export default { | ||||
|  | ||||
|             this.listenTo(window, 'mouseup', this.onMouseUp, this); | ||||
|             this.listenTo(window, 'mousemove', this.trackMousePosition, this); | ||||
|  | ||||
|             // track frozen state on mouseDown to be read on mouseUp | ||||
|             const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; | ||||
|             this.isFrozenOnMouseDown = isFrozen; | ||||
|  | ||||
|             if (event.altKey) { | ||||
|                 return this.startPan(event); | ||||
|             } else { | ||||
| @@ -706,7 +723,14 @@ export default { | ||||
|             } | ||||
|  | ||||
|             if (this.marquee) { | ||||
|                 return this.endMarquee(event); | ||||
|                 this.endMarquee(event); | ||||
|             } | ||||
|  | ||||
|             // resume the plot if no pan, zoom, or drag action is taken | ||||
|             // needs to follow endMarquee so that plotHistory is pruned | ||||
|             const isAction = Boolean(this.plotHistory.length); | ||||
|             if (!isAction && !this.isFrozenOnMouseDown) { | ||||
|                 return this.play(); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,9 @@ export default function PlotViewProvider(openmct) { | ||||
|     } | ||||
|  | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|   | ||||
| @@ -1,346 +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 BarGraphCompositionPolicy from "./BarGraphCompositionPolicy"; | ||||
| import { createOpenMct } from "utils/testing"; | ||||
|  | ||||
| describe("The bar graph composition policy", () => { | ||||
|     let openmct; | ||||
|     const mockMetaDataWithNoRangeHints = { | ||||
|         "period": 10, | ||||
|         "amplitude": 1, | ||||
|         "offset": 0, | ||||
|         "dataRateInHz": 1, | ||||
|         "phase": 0, | ||||
|         "randomness": 0, | ||||
|         valuesForHints: () => { | ||||
|             return []; | ||||
|         }, | ||||
|         values: [ | ||||
|             { | ||||
|                 "key": "name", | ||||
|                 "name": "Name", | ||||
|                 "format": "string" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "utc", | ||||
|                 "name": "Time", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 1, | ||||
|                     "priority": 1 | ||||
|                 }, | ||||
|                 "source": "utc" | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|     const mockMetaDataWithRangeHints = { | ||||
|         "period": 10, | ||||
|         "amplitude": 1, | ||||
|         "offset": 0, | ||||
|         "dataRateInHz": 1, | ||||
|         "phase": 0, | ||||
|         "randomness": 0, | ||||
|         "wavelength": 0, | ||||
|         valuesForHints: () => { | ||||
|             return [ | ||||
|                 { | ||||
|                     "key": "sin", | ||||
|                     "name": "Sine", | ||||
|                     "unit": "Hz", | ||||
|                     "formatString": "%0.2f", | ||||
|                     "hints": { | ||||
|                         "range": 1, | ||||
|                         "priority": 4 | ||||
|                     }, | ||||
|                     "source": "sin" | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "cos", | ||||
|                     "name": "Cosine", | ||||
|                     "unit": "deg", | ||||
|                     "formatString": "%0.2f", | ||||
|                     "hints": { | ||||
|                         "range": 2, | ||||
|                         "priority": 5 | ||||
|                     }, | ||||
|                     "source": "cos" | ||||
|                 } | ||||
|             ]; | ||||
|         }, | ||||
|         values: [ | ||||
|             { | ||||
|                 "key": "name", | ||||
|                 "name": "Name", | ||||
|                 "format": "string", | ||||
|                 "source": "name", | ||||
|                 "hints": { | ||||
|                     "priority": 0 | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 "key": "utc", | ||||
|                 "name": "Time", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 1, | ||||
|                     "priority": 1 | ||||
|                 }, | ||||
|                 "source": "utc" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "yesterday", | ||||
|                 "name": "Yesterday", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 2, | ||||
|                     "priority": 2 | ||||
|                 }, | ||||
|                 "source": "yesterday" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "sin", | ||||
|                 "name": "Sine", | ||||
|                 "unit": "Hz", | ||||
|                 "formatString": "%0.2f", | ||||
|                 "hints": { | ||||
|                     "range": 1, | ||||
|                     "spectralAttribute": true | ||||
|                 }, | ||||
|                 "source": "sin" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "cos", | ||||
|                 "name": "Cosine", | ||||
|                 "unit": "deg", | ||||
|                 "formatString": "%0.2f", | ||||
|                 "hints": { | ||||
|                     "range": 2, | ||||
|                     "priority": 5 | ||||
|                 }, | ||||
|                 "source": "cos" | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.telemetry.isTelemetryObject = function (domainObject) { | ||||
|             return true; | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     it("exists", () => { | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     xit("allow composition for telemetry that provides/supports bar graph meta data", () => { | ||||
|         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 child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
|  | ||||
|     it("allows composition for telemetry that contain at least one range", () => { | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|         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 child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
|  | ||||
|     it("disallows composition for telemetry that don't contain any range hints", () => { | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithNoRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|         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 child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false); | ||||
|     }); | ||||
|  | ||||
|     it("passthrough for composition for non bar graph plots", () => { | ||||
|         const parent = { | ||||
|             "composition": [], | ||||
|             "configuration": {}, | ||||
|             "name": "Some Stacked Plot", | ||||
|             "type": "telemetry.plot.stacked", | ||||
|             "location": "mine", | ||||
|             "modified": 1631005183584, | ||||
|             "persisted": 1631005183502, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|             } | ||||
|         }; | ||||
|         const child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @@ -82,12 +82,17 @@ export default class PlotSeries extends Model { | ||||
|             .openmct | ||||
|             .telemetry | ||||
|             .getMetadata(options.domainObject); | ||||
|  | ||||
|         this.formats = options | ||||
|             .openmct | ||||
|             .telemetry | ||||
|             .getFormatMap(this.metadata); | ||||
|  | ||||
|         const range = this.metadata.valuesForHints(['range'])[0]; | ||||
|         //if the object is missing or doesn't have metadata for some reason | ||||
|         let range = {}; | ||||
|         if (this.metadata) { | ||||
|             range = this.metadata.valuesForHints(['range'])[0]; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             name: options.domainObject.name, | ||||
| @@ -191,7 +196,10 @@ export default class PlotSeries extends Model { | ||||
|                     .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) | ||||
|                     .value(); | ||||
|                 this.reset(newPoints); | ||||
|             }.bind(this)); | ||||
|             }.bind(this)) | ||||
|             .catch((error) => { | ||||
|                 console.warn('Error fetching data', error); | ||||
|             }); | ||||
|         /* eslint-enable you-dont-need-lodash-underscore/concat */ | ||||
|     } | ||||
|     /** | ||||
| @@ -199,7 +207,9 @@ export default class PlotSeries extends Model { | ||||
|      */ | ||||
|     onXKeyChange(xKey) { | ||||
|         const format = this.formats[xKey]; | ||||
|         this.getXVal = format.parse.bind(format); | ||||
|         if (format) { | ||||
|             this.getXVal = format.parse.bind(format); | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * Update y formatter on change, default to stepAfter interpolation if | ||||
|   | ||||
| @@ -23,8 +23,8 @@ import _ from 'lodash'; | ||||
|  | ||||
| import PlotSeries from "./PlotSeries"; | ||||
| import Collection from "./Collection"; | ||||
| import Color from "../lib/Color"; | ||||
| import ColorPalette from "../lib/ColorPalette"; | ||||
| import Color from "@/ui/color/Color"; | ||||
| import ColorPalette from "@/ui/color/ColorPalette"; | ||||
|  | ||||
| export default class SeriesCollection extends Collection { | ||||
|  | ||||
|   | ||||
| @@ -184,7 +184,7 @@ export default class YAxisModel extends Model { | ||||
|         this.set('values', yMetadata.values); | ||||
|         if (!label) { | ||||
|             const labelName = series.map(function (s) { | ||||
|                 return s.metadata.value(s.get('yKey')).name; | ||||
|                 return s.metadata ? s.metadata.value(s.get('yKey')).name : ''; | ||||
|             }).reduce(function (a, b) { | ||||
|                 if (a === undefined) { | ||||
|                     return b; | ||||
| @@ -204,7 +204,7 @@ export default class YAxisModel extends Model { | ||||
|             } | ||||
|  | ||||
|             const labelUnits = series.map(function (s) { | ||||
|                 return s.metadata.value(s.get('yKey')).units; | ||||
|                 return s.metadata ? s.metadata.value(s.get('yKey')).units : ''; | ||||
|             }).reduce(function (a, b) { | ||||
|                 if (a === undefined) { | ||||
|                     return b; | ||||
|   | ||||
| @@ -79,7 +79,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorSwatch from "@/plugins/plot/ColorSwatch.vue"; | ||||
| import ColorSwatch from '@/ui/color/ColorSwatch.vue'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -129,7 +129,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorSwatch from '../../ColorSwatch.vue'; | ||||
| import ColorSwatch from '@/ui/color/ColorSwatch.vue'; | ||||
| import { MARKER_SHAPES } from "../../draw/MarkerShapes"; | ||||
| import { objectPath, validate, coerce } from "./formUtil"; | ||||
| import _ from 'lodash'; | ||||
|   | ||||
| @@ -25,7 +25,9 @@ import Vue from 'vue'; | ||||
|  | ||||
| export default function OverlayPlotViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|   | ||||
| @@ -19,18 +19,12 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { BAR_GRAPH_KEY } from './barGraph/BarGraphConstants'; | ||||
| import PlotViewProvider from './PlotViewProvider'; | ||||
| import SpectralPlotViewProvider from './spectralPlot/SpectralPlotViewProvider'; | ||||
| import BarGraphViewProvider from './barGraph/BarGraphViewProvider'; | ||||
| import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider'; | ||||
| import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider'; | ||||
| import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider'; | ||||
| import BarGraphInspectorViewProvider from './barGraph/inspector/BarGraphInspectorViewProvider'; | ||||
| import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; | ||||
| import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; | ||||
| import SpectralPlotCompositionPolicy from './spectralPlot/SpectralPlotCompositionPolicy'; | ||||
| import BarGraphCompositionPolicy from './barGraph/BarGraphCompositionPolicy'; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
| @@ -64,48 +58,15 @@ export default function () { | ||||
|             }, | ||||
|             priority: 890 | ||||
|         }); | ||||
|         openmct.types.addType('telemetry.plot.spectral', { | ||||
|             key: "telemetry.plot.spectral", | ||||
|             name: "Spectral Plot", | ||||
|             cssClass: "icon-plot-stacked", | ||||
|             description: "View Spectra on Y Axes with non-time domain on the X axis. Can be added to Display Layouts.", | ||||
|             //Temporarily disabling spectral plots | ||||
|             creatable: false, | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = {}; | ||||
|             }, | ||||
|             priority: 890 | ||||
|         }); | ||||
|  | ||||
|         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 = { | ||||
|                     plotType: 'bar' | ||||
|                 }; | ||||
|             }, | ||||
|             priority: 891 | ||||
|         }); | ||||
|  | ||||
|         openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); | ||||
|         openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); | ||||
|         openmct.objectViews.addProvider(new PlotViewProvider(openmct)); | ||||
|         openmct.objectViews.addProvider(new SpectralPlotViewProvider(openmct)); | ||||
|         openmct.objectViews.addProvider(new BarGraphViewProvider(openmct)); | ||||
|  | ||||
|         openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct)); | ||||
|         openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct)); | ||||
|  | ||||
|         openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); | ||||
|         openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); | ||||
|         openmct.composition.addPolicy(new SpectralPlotCompositionPolicy(openmct).allow); | ||||
|         openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow); | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -24,12 +24,10 @@ import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} f | ||||
| import PlotVuePlugin from "./plugin"; | ||||
| import Vue from "vue"; | ||||
| import StackedPlot from "./stackedPlot/StackedPlot.vue"; | ||||
| // import SpectralPlot from "./spectralPlot/SpectralPlot.vue"; | ||||
| import configStore from "./configuration/ConfigStore"; | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import PlotOptions from "./inspector/PlotOptions.vue"; | ||||
| import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; | ||||
| import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './barGraph/BarGraphConstants'; | ||||
|  | ||||
| describe("the plugin", function () { | ||||
|     let element; | ||||
| @@ -143,6 +141,7 @@ describe("the plugin", function () { | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             unobserve() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
| @@ -315,37 +314,6 @@ describe("the plugin", function () { | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("provides a spectral plot view for objects with telemetry", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "telemetry.plot.spectral", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "a-very-fine-key" | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-spectral"); | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("provides a spectral aggregate plot view for objects with telemetry", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
|                 type: BAR_GRAPH_KEY, | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "lots-of-aggregate-telemetry" | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             let plotView = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW); | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The single plot view", () => { | ||||
| @@ -402,6 +370,10 @@ describe("the plugin", function () { | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         it("Makes only one request for telemetry on load", () => { | ||||
|             expect(openmct.telemetry.request).toHaveBeenCalledTimes(1); | ||||
|         }); | ||||
|  | ||||
|         it("Renders a collapsed legend for every telemetry", () => { | ||||
|             let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); | ||||
|             expect(legend.length).toBe(1); | ||||
| @@ -476,6 +448,64 @@ describe("the plugin", function () { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('resume actions on errant click', () => { | ||||
|             beforeEach(() => { | ||||
|                 openmct.time.clock('local', { | ||||
|                     start: -1000, | ||||
|                     end: 100 | ||||
|                 }); | ||||
|  | ||||
|                 return Vue.nextTick(); | ||||
|             }); | ||||
|  | ||||
|             it("clicking the plot view without movement resumes the plot while active", async () => { | ||||
|  | ||||
|                 const pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); | ||||
|                 // if the pause button is present, the chart is running | ||||
|                 expect(pauseEl.length).toBe(1); | ||||
|  | ||||
|                 // simulate an errant mouse click | ||||
|                 // the second item is the canvas we need to use | ||||
|                 const canvas = element.querySelectorAll("canvas")[1]; | ||||
|                 const mouseDownEvent = new MouseEvent('mousedown'); | ||||
|                 const mouseUpEvent = new MouseEvent('mouseup'); | ||||
|                 canvas.dispatchEvent(mouseDownEvent); | ||||
|                 // mouseup event is bound to the window | ||||
|                 window.dispatchEvent(mouseUpEvent); | ||||
|                 await Vue.nextTick(); | ||||
|  | ||||
|                 const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause"); | ||||
|                 console.log('pauseElAfterClick', pauseElAfterClick); | ||||
|                 expect(pauseElAfterClick.length).toBe(1); | ||||
|  | ||||
|             }); | ||||
|  | ||||
|             it("clicking the plot view without movement leaves the plot paused", async () => { | ||||
|  | ||||
|                 const pauseEl = element.querySelector(".c-button-set .icon-pause"); | ||||
|                 // pause the plot | ||||
|                 pauseEl.dispatchEvent(createMouseEvent('click')); | ||||
|                 await Vue.nextTick(); | ||||
|  | ||||
|                 const playEl = element.querySelectorAll('.c-button-set .is-paused'); | ||||
|                 expect(playEl.length).toBe(1); | ||||
|  | ||||
|                 // simulate an errant mouse click | ||||
|                 // the second item is the canvas we need to use | ||||
|                 const canvas = element.querySelectorAll("canvas")[1]; | ||||
|                 const mouseDownEvent = new MouseEvent('mousedown'); | ||||
|                 const mouseUpEvent = new MouseEvent('mouseup'); | ||||
|                 canvas.dispatchEvent(mouseDownEvent); | ||||
|                 // mouseup event is bound to the window | ||||
|                 window.dispatchEvent(mouseUpEvent); | ||||
|                 await Vue.nextTick(); | ||||
|  | ||||
|                 const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused"); | ||||
|                 expect(playElAfterChartClick.length).toBe(1); | ||||
|  | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('controls in time strip view', () => { | ||||
|  | ||||
|             it('zoom controls are hidden', () => { | ||||
| @@ -496,146 +526,6 @@ describe("the plugin", function () { | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     /* | ||||
|     * disabling this until we develop the plot view | ||||
|     describe("The spectral plot view", () => { | ||||
|         let testTelemetryObject; | ||||
|         // eslint-disable-next-line no-unused-vars | ||||
|         let testTelemetryObject2; | ||||
|         // eslint-disable-next-line no-unused-vars | ||||
|         let config; | ||||
|         let spectralPlotObject; | ||||
|         let component; | ||||
|         let mockComposition; | ||||
|         // eslint-disable-next-line no-unused-vars | ||||
|         let plotViewComponentObject; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             const getFunc = openmct.$injector.get; | ||||
|             spyOn(openmct.$injector, "get") | ||||
|                 .withArgs("exportImageService").and.returnValue({ | ||||
|                     exportPNG: () => {}, | ||||
|                     exportJPG: () => {} | ||||
|                 }) | ||||
|                 .and.callFake(getFunc); | ||||
|  | ||||
|             spectralPlotObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-spectral-plot" | ||||
|                 }, | ||||
|                 type: "telemetry.plot.spectral", | ||||
|                 name: "Test Spectral Plot" | ||||
|             }; | ||||
|  | ||||
|             testTelemetryObject = { | ||||
|                 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 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             testTelemetryObject2 = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object2" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object2", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "wavelength", | ||||
|                         name: "Wavelength", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key2", | ||||
|                         name: "Another attribute2", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testTelemetryObject); | ||||
|  | ||||
|                 return [testTelemetryObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             let viewContainer = document.createElement("div"); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     SpectralPlot | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     domainObject: spectralPlotObject, | ||||
|                     composition: openmct.composition.get(spectralPlotObject) | ||||
|                 }, | ||||
|                 template: "<spectral-plot></spectral-plot>" | ||||
|             }); | ||||
|  | ||||
|             cleanupFirst.push(() => { | ||||
|                 component.$destroy(); | ||||
|                 component = undefined; | ||||
|             }); | ||||
|  | ||||
|             return telemetryPromise | ||||
|                 .then(Vue.nextTick()) | ||||
|                 .then(() => { | ||||
|                     plotViewComponentObject = component.$root.$children[0]; | ||||
|                     const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); | ||||
|                     config = configStore.get(configId); | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         it("Renders a collapsed legend for every telemetry", () => { | ||||
|             let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); | ||||
|             expect(legend.length).toBe(1); | ||||
|             expect(legend[0].innerHTML).toEqual("Test Object"); | ||||
|         }); | ||||
|  | ||||
|     }); */ | ||||
|  | ||||
|     describe("The stacked plot view", () => { | ||||
|         let testTelemetryObject; | ||||
|         let testTelemetryObject2; | ||||
| @@ -1162,42 +1052,11 @@ describe("the plugin", function () { | ||||
|                 const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); | ||||
|                 expect(yAxisProperties.length).toEqual(3); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the spectral plot", () => { | ||||
|         const mockObject = { | ||||
|             name: 'A Very Nice Spectral Plot', | ||||
|             key: 'telemetry.plot.spectral', | ||||
|             creatable: true | ||||
|         }; | ||||
|  | ||||
|         it('defines a spectral plot object type with the correct key', () => { | ||||
|             const objectDef = openmct.types.get('telemetry.plot.spectral').definition; | ||||
|             expect(objectDef.key).toEqual(mockObject.key); | ||||
|         }); | ||||
|  | ||||
|         xit('is creatable', () => { | ||||
|             const objectDef = openmct.types.get('telemetry.plot.spectral').definition; | ||||
|             expect(objectDef.creatable).toEqual(mockObject.creatable); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the aggregate spectral plot", () => { | ||||
|         const mockObject = { | ||||
|             name: 'An Even Nicer Aggregate Spectral Plot', | ||||
|             key: BAR_GRAPH_KEY, | ||||
|             creatable: true | ||||
|         }; | ||||
|  | ||||
|         it('defines a spectral plot 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); | ||||
|             it('renders color palette options', () => { | ||||
|                 const colorSwatch = editOptionsEl.querySelector(".c-click-swatch"); | ||||
|                 expect(colorSwatch).toBeDefined(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| export default function SpectralPlotCompositionPolicy(openmct) { | ||||
|     function hasSpectralDomainAndRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
|         const domainValues = metadata.valuesForHints(['domain']); | ||||
|         const containsSomeSpectralData = domainValues.some(value => { | ||||
|             return ((value.key === 'wavelength') || (value.key === 'frequency')); | ||||
|         }); | ||||
|  | ||||
|         return rangeValues.length > 0 | ||||
|         && domainValues.length > 0 | ||||
|         && containsSomeSpectralData; | ||||
|     } | ||||
|  | ||||
|     function hasSpectralTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasSpectralDomainAndRange(metadata); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         allow: function (parent, child) { | ||||
|  | ||||
|             if ((parent.type === 'telemetry.plot.spectral') | ||||
|                 && ((child.type !== 'telemetry.plot.overlay') && (hasSpectralTelemetry(child) === false)) | ||||
|             ) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import SpectralView from './SpectralView.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function SpectralPlotViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: 'plot-spectral', | ||||
|         name: 'Spectral Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === 'telemetry.plot.spectral'; | ||||
|         }, | ||||
|  | ||||
|         canEdit(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === 'telemetry.plot.spectral'; | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     let isCompact = isCompactView(objectPath); | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             SpectralView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 options: { | ||||
|                                     compact: isCompact | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<spectral-view :options="options"></spectral-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| <template> | ||||
| <div> | ||||
|  | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'] | ||||
| }; | ||||
|  | ||||
| </script> | ||||
| @@ -90,7 +90,8 @@ export default { | ||||
|             cursorGuide: false, | ||||
|             gridLines: true, | ||||
|             loading: false, | ||||
|             compositionObjects: [] | ||||
|             compositionObjects: [], | ||||
|             tickWidthMap: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -106,8 +107,6 @@ export default { | ||||
|  | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|  | ||||
|         this.tickWidthMap = {}; | ||||
|  | ||||
|         this.composition.on('add', this.addChild); | ||||
|         this.composition.on('remove', this.removeChild); | ||||
|         this.composition.on('reorder', this.compositionReorder); | ||||
| @@ -127,13 +126,15 @@ export default { | ||||
|         addChild(child) { | ||||
|             const id = this.openmct.objects.makeKeyString(child.identifier); | ||||
|  | ||||
|             this.tickWidthMap[id] = 0; | ||||
|             this.$set(this.tickWidthMap, id, 0); | ||||
|             this.compositionObjects.push(child); | ||||
|         }, | ||||
|  | ||||
|         removeChild(childIdentifier) { | ||||
|             const id = this.openmct.objects.makeKeyString(childIdentifier); | ||||
|             delete this.tickWidthMap[id]; | ||||
|  | ||||
|             this.$delete(this.tickWidthMap, id); | ||||
|  | ||||
|             const childObj = this.compositionObjects.filter((c) => { | ||||
|                 const identifier = this.openmct.objects.makeKeyString(c.identifier); | ||||
|  | ||||
| @@ -191,14 +192,7 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             //update the tickWidth for this plotId, the computed max tick width of the stacked plot will be cascaded down | ||||
|             //TODO: Might need to do this using $set | ||||
|             this.tickWidthMap[plotId] = Math.max(width, this.tickWidthMap[plotId]); | ||||
|             // const newTickWidth = Math.max(...Object.values(this.tickWidthMap)); | ||||
|             // if (newTickWidth !== tickWidth || width !== tickWidth) { | ||||
|             //     tickWidth = newTickWidth; | ||||
|             //     $scope.$broadcast('plot:tickWidth', tickWidth); | ||||
|             // } | ||||
|             this.$set(this.tickWidthMap, plotId, width); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -25,7 +25,9 @@ import Vue from 'vue'; | ||||
|  | ||||
| export default function StackedPlotViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|   | ||||
| @@ -36,6 +36,7 @@ define([ | ||||
|     './URLIndicatorPlugin/URLIndicatorPlugin', | ||||
|     './telemetryMean/plugin', | ||||
|     './plot/plugin', | ||||
|     './charts/plugin', | ||||
|     './telemetryTable/plugin', | ||||
|     './staticRootPlugin/plugin', | ||||
|     './notebook/plugin', | ||||
| @@ -87,6 +88,7 @@ define([ | ||||
|     URLIndicatorPlugin, | ||||
|     TelemetryMean, | ||||
|     PlotPlugin, | ||||
|     ChartPlugin, | ||||
|     TelemetryTablePlugin, | ||||
|     StaticRootPlugin, | ||||
|     Notebook, | ||||
| @@ -189,6 +191,7 @@ define([ | ||||
|     plugins.ExampleImagery = ExampleImagery; | ||||
|     plugins.ImageryPlugin = ImageryPlugin; | ||||
|     plugins.Plot = PlotPlugin.default; | ||||
|     plugins.Chart = ChartPlugin.default; | ||||
|     plugins.TelemetryTable = TelemetryTablePlugin; | ||||
|  | ||||
|     plugins.SummaryWidget = SummaryWidget; | ||||
|   | ||||
| @@ -7,7 +7,8 @@ define([ | ||||
|     './eventHelpers', | ||||
|     'objectUtils', | ||||
|     'lodash', | ||||
|     'zepto' | ||||
|     'zepto', | ||||
|     '@braintree/sanitize-url' | ||||
| ], function ( | ||||
|     widgetTemplate, | ||||
|     Rule, | ||||
| @@ -17,7 +18,8 @@ define([ | ||||
|     eventHelpers, | ||||
|     objectUtils, | ||||
|     _, | ||||
|     $ | ||||
|     $, | ||||
|     urlSanitizeLib | ||||
| ) { | ||||
|  | ||||
|     //default css configuration for new rules | ||||
| @@ -88,7 +90,7 @@ define([ | ||||
|         function toggleTestData() { | ||||
|             self.outerWrapper.toggleClass('expanded-widget-test-data'); | ||||
|             self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded'); | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); | ||||
|  | ||||
| @@ -99,7 +101,7 @@ define([ | ||||
|         function toggleRules() { | ||||
|             self.outerWrapper.toggleClass('expanded-widget-rules'); | ||||
|             self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded'); | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         this.listenTo(this.toggleRulesControl, 'click', toggleRules); | ||||
|  | ||||
| @@ -114,7 +116,7 @@ define([ | ||||
|      */ | ||||
|     SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { | ||||
|         if (url) { | ||||
|             this.widgetButton.attr('href', url); | ||||
|             this.widgetButton.attr('href', urlSanitizeLib.sanitizeUrl(url)); | ||||
|         } else { | ||||
|             this.widgetButton.removeAttr('href'); | ||||
|         } | ||||
| @@ -317,7 +319,7 @@ define([ | ||||
|                 expanded: 'true' | ||||
|             }; | ||||
|  | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; | ||||
|         this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct, | ||||
| @@ -345,7 +347,7 @@ define([ | ||||
|             ruleOrder.splice(targetIndex + 1, 0, event.draggingId); | ||||
|             this.domainObject.configuration.ruleOrder = ruleOrder; | ||||
|             this.updateDomainObject(); | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         this.refreshRules(); | ||||
|     }; | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| define([ | ||||
|     './summary-widget.html' | ||||
|     './summary-widget.html', | ||||
|     '@braintree/sanitize-url' | ||||
| ], function ( | ||||
|     summaryWidgetTemplate | ||||
|     summaryWidgetTemplate, | ||||
|     urlSanitizeLib | ||||
| ) { | ||||
|     const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; | ||||
|  | ||||
| @@ -35,8 +37,9 @@ define([ | ||||
|         this.icon = this.container.querySelector('#widgetIcon'); | ||||
|         this.label = this.container.querySelector('.js-sw__label'); | ||||
|  | ||||
|         if (this.domainObject.url) { | ||||
|             this.widget.setAttribute('href', this.domainObject.url); | ||||
|         let url = this.domainObject.url; | ||||
|         if (url) { | ||||
|             this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url)); | ||||
|         } else { | ||||
|             this.widget.removeAttribute('href'); | ||||
|         } | ||||
|   | ||||
| @@ -60,18 +60,17 @@ define([ | ||||
|             this.addTelemetryObject = this.addTelemetryObject.bind(this); | ||||
|             this.removeTelemetryObject = this.removeTelemetryObject.bind(this); | ||||
|             this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); | ||||
|             this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); | ||||
|             this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); | ||||
|             this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); | ||||
|             this.isTelemetryObject = this.isTelemetryObject.bind(this); | ||||
|             this.refreshData = this.refreshData.bind(this); | ||||
|             this.updateFilters = this.updateFilters.bind(this); | ||||
|             this.clearData = this.clearData.bind(this); | ||||
|             this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); | ||||
|  | ||||
|             this.filterObserver = undefined; | ||||
|  | ||||
|             this.createTableRowCollections(); | ||||
|  | ||||
|             openmct.time.on('bounds', this.refreshData); | ||||
|             openmct.time.on('timeSystem', this.refreshData); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -141,8 +140,6 @@ define([ | ||||
|             let columnMap = this.getColumnMapForObject(keyString); | ||||
|             let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); | ||||
|  | ||||
|             this.incrementOutstandingRequests(); | ||||
|  | ||||
|             const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); | ||||
|             const telemetryRemover = this.getTelemetryRemover(); | ||||
|  | ||||
| @@ -151,13 +148,13 @@ define([ | ||||
|             this.telemetryCollections[keyString] = this.openmct.telemetry | ||||
|                 .requestCollection(telemetryObject, requestOptions); | ||||
|  | ||||
|             this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); | ||||
|             this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); | ||||
|             this.telemetryCollections[keyString].on('remove', telemetryRemover); | ||||
|             this.telemetryCollections[keyString].on('add', telemetryProcessor); | ||||
|             this.telemetryCollections[keyString].on('clear', this.tableRows.clear); | ||||
|             this.telemetryCollections[keyString].on('clear', this.clearData); | ||||
|             this.telemetryCollections[keyString].load(); | ||||
|  | ||||
|             this.decrementOutstandingRequests(); | ||||
|  | ||||
|             this.telemetryObjects[keyString] = { | ||||
|                 telemetryObject, | ||||
|                 keyString, | ||||
| @@ -268,17 +265,6 @@ define([ | ||||
|             this.emit('object-removed', objectIdentifier); | ||||
|         } | ||||
|  | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick && this.tableRows.outstandingRequests === 0) { | ||||
|                 this.tableRows.clear(); | ||||
|                 this.tableRows.sortBy({ | ||||
|                     key: this.openmct.time.timeSystem().key, | ||||
|                     direction: 'asc' | ||||
|                 }); | ||||
|                 this.tableRows.resubscribe(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         clearData() { | ||||
|             this.tableRows.clear(); | ||||
|             this.emit('refresh'); | ||||
| @@ -378,9 +364,6 @@ define([ | ||||
|             let keystrings = Object.keys(this.telemetryCollections); | ||||
|             keystrings.forEach(this.removeTelemetryCollection); | ||||
|  | ||||
|             this.openmct.time.off('bounds', this.refreshData); | ||||
|             this.openmct.time.off('timeSystem', this.refreshData); | ||||
|  | ||||
|             if (this.filterObserver) { | ||||
|                 this.filterObserver(); | ||||
|             } | ||||
|   | ||||
| @@ -131,7 +131,8 @@ export default { | ||||
|             objects.forEach(object => this.addColumnsForObject(object, false)); | ||||
|         }, | ||||
|         addColumnsForObject(telemetryObject) { | ||||
|             let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             let metadataValues = metadata ? metadata.values() : []; | ||||
|             metadataValues.forEach(metadatum => { | ||||
|                 let column = new TelemetryTableColumn(this.openmct, metadatum); | ||||
|                 this.tableConfiguration.addSingleColumnForObject(telemetryObject, column); | ||||
|   | ||||
| @@ -105,7 +105,8 @@ export default { | ||||
|                 composition.load().then((domainObjects) => { | ||||
|                     domainObjects.forEach(telemetryObject => { | ||||
|                         let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|                         let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); | ||||
|                         const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|                         let metadataValues = metadata ? metadata.values() : []; | ||||
|                         let filters = this.filteredTelemetry[keyString]; | ||||
|  | ||||
|                         if (filters !== undefined) { | ||||
|   | ||||
| @@ -125,7 +125,6 @@ | ||||
|     <div | ||||
|         class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver" | ||||
|         :class="{ | ||||
|             'loading': loading, | ||||
|             'is-paused' : paused | ||||
|         }" | ||||
|     > | ||||
| @@ -362,7 +361,7 @@ export default { | ||||
|             autoScroll: true, | ||||
|             sortOptions: {}, | ||||
|             filters: {}, | ||||
|             loading: true, | ||||
|             loading: false, | ||||
|             scrollable: undefined, | ||||
|             tableEl: undefined, | ||||
|             headersHolderEl: undefined, | ||||
| @@ -422,6 +421,14 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         loading: { | ||||
|             handler(isLoading) { | ||||
|                 if (this.viewActionsCollection) { | ||||
|                     let action = isLoading ? 'disable' : 'enable'; | ||||
|                     this.viewActionsCollection[action](['export-csv-all']); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         markedRows: { | ||||
|             handler(newVal, oldVal) { | ||||
|                 this.$emit('marked-rows-updated', newVal, oldVal); | ||||
| @@ -1020,6 +1027,12 @@ export default { | ||||
|                 this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']); | ||||
|             } | ||||
|  | ||||
|             if (this.loading) { | ||||
|                 this.viewActionsCollection.disable(['export-csv-all']); | ||||
|             } else { | ||||
|                 this.viewActionsCollection.enable(['export-csv-all']); | ||||
|             } | ||||
|  | ||||
|             if (this.paused) { | ||||
|                 this.viewActionsCollection.hide(['pause-data']); | ||||
|                 this.viewActionsCollection.show(['play-data']); | ||||
|   | ||||
| @@ -222,6 +222,24 @@ describe("the plugin", () => { | ||||
|             openmct.router.path = originalRouterPath; | ||||
|         }); | ||||
|  | ||||
|         it("Shows no progress bar initially", () => { | ||||
|             let progressBar = element.querySelector('.c-progress-bar'); | ||||
|  | ||||
|             expect(tableInstance.outstandingRequests).toBe(0); | ||||
|             expect(progressBar).toBeNull(); | ||||
|         }); | ||||
|  | ||||
|         it("Shows a progress bar while making requests", async () => { | ||||
|             tableInstance.incrementOutstandingRequests(); | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             let progressBar = element.querySelector('.c-progress-bar'); | ||||
|  | ||||
|             expect(tableInstance.outstandingRequests).toBe(1); | ||||
|             expect(progressBar).not.toBeNull(); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it("Renders a row for every telemetry datum returned", () => { | ||||
|             let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); | ||||
|             expect(rows.length).toBe(3); | ||||
|   | ||||
| @@ -231,8 +231,8 @@ export default { | ||||
|             const panStart = bounds.start - percX * deltaTime; | ||||
|  | ||||
|             return { | ||||
|                 start: panStart, | ||||
|                 end: panStart + deltaTime | ||||
|                 start: parseInt(panStart, 10), | ||||
|                 end: parseInt(panStart + deltaTime, 10) | ||||
|             }; | ||||
|         }, | ||||
|         startZoom() { | ||||
| @@ -296,7 +296,7 @@ export default { | ||||
|             const valueDelta = value - this.left; | ||||
|             const offset = valueDelta / this.width * timeDelta; | ||||
|  | ||||
|             return bounds.start + offset; | ||||
|             return parseInt(bounds.start + offset, 10); | ||||
|         }, | ||||
|         isChangingViewBounds() { | ||||
|             return this.dragStartX && this.dragX && this.dragStartX !== this.dragX; | ||||
|   | ||||
| @@ -1,13 +1,7 @@ | ||||
| <template> | ||||
| <form ref="fixedDeltaInput" | ||||
|       class="c-conductor__inputs" | ||||
|       @submit.prevent="updateTimeFromConductor" | ||||
| > | ||||
|     <button | ||||
|         ref="submitButton" | ||||
|         class="c-input--submit" | ||||
|         type="submit" | ||||
|     ></button> | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" | ||||
|     > | ||||
| @@ -56,10 +50,6 @@ | ||||
|             @date-selected="endDateSelected" | ||||
|         /> | ||||
|     </div> | ||||
|     <input | ||||
|         type="submit" | ||||
|         class="invisible" | ||||
|     > | ||||
| </form> | ||||
| </template> | ||||
|  | ||||
| @@ -183,10 +173,7 @@ export default { | ||||
|         submitForm() { | ||||
|         // Allow Vue model to catch up to user input. | ||||
|         // Submitting form will cause validation messages to display (but only if triggered by button click) | ||||
|             this.$nextTick(() => this.$refs.submitButton.click()); | ||||
|         }, | ||||
|         updateTimeFromConductor() { | ||||
|             this.setBoundsFromView(); | ||||
|             this.$nextTick(() => this.setBoundsFromView()); | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|   | ||||
| @@ -151,29 +151,22 @@ export default { | ||||
|             this.stopFollowingTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getContextForView([this.domainObject]); | ||||
|             this.timeContext.on('timeContext', this.setTimeContext); | ||||
|             this.timeContext.on('clock', this.setViewFromClock); | ||||
|             this.timeContext.on('clock', this.setTimeOptions); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('timeContext', this.setTimeContext); | ||||
|                 this.timeContext.off('clock', this.setViewFromClock); | ||||
|                 this.timeContext.off('clock', this.setTimeOptions); | ||||
|             } | ||||
|         }, | ||||
|         setViewFromClock(clock) { | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 this.setTimeOptions(clock); | ||||
|             } | ||||
|         }, | ||||
|         setTimeOptions() { | ||||
|             if (!this.timeOptions || !this.timeOptions.mode) { | ||||
|                 this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key}; | ||||
|                 this.timeOptions = { | ||||
|                     clockOffsets: this.timeContext.clockOffsets(), | ||||
|                     fixedOffsets: this.timeContext.bounds() | ||||
|                 }; | ||||
|             } | ||||
|         setTimeOptions(clock) { | ||||
|             this.timeOptions.clockOffsets = this.timeOptions.clockOffsets || this.timeContext.clockOffsets(); | ||||
|             this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds(); | ||||
|  | ||||
|             this.registerIndependentTimeOffsets(); | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 this.mode = this.timeContext.clock() === undefined ? {key: 'fixed'} : {key: Object.create(this.timeContext.clock()).key}; | ||||
|                 this.registerIndependentTimeOffsets(); | ||||
|             } | ||||
|         }, | ||||
|         saveFixedOffsets(offsets) { | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|   | ||||
| @@ -20,7 +20,8 @@ | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
| <template> | ||||
| <div ref="modeMenuButton" | ||||
| <div v-if="modes.length > 1" | ||||
|      ref="modeMenuButton" | ||||
|      class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" | ||||
| > | ||||
|     <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> | ||||
|   | ||||
| @@ -88,7 +88,7 @@ export default { | ||||
|             this.mutablePromise.then(() => { | ||||
|                 this.openmct.objects.destroyMutable(this.domainObject); | ||||
|             }); | ||||
|         } else { | ||||
|         } else if (this.domainObject.isMutable) { | ||||
|             this.openmct.objects.destroyMutable(this.domainObject); | ||||
|         } | ||||
|     }, | ||||
|   | ||||
| @@ -1,16 +1,23 @@ | ||||
| <template> | ||||
| <div class="l-iframe abs"> | ||||
|     <iframe :src="currentDomainObject.url"></iframe> | ||||
|     <iframe :src="url"></iframe> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data: function () { | ||||
|         return { | ||||
|             currentDomainObject: this.domainObject | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         url() { | ||||
|             return sanitizeUrl(this.currentDomainObject.url); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -729,7 +729,7 @@ mct-plot { | ||||
|  | ||||
| } | ||||
|  | ||||
| /********************************************************************* BAR CHARTS */ | ||||
| /***************** BAR GRAPHS */ | ||||
| .c-bar-chart { | ||||
|     flex: 1 1 auto; | ||||
|     overflow: hidden; | ||||
|   | ||||
| @@ -40,11 +40,6 @@ | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .zerolinelayer { | ||||
|             // Hide unneeded plotly-styled horizontal line | ||||
|             display: none; | ||||
|         } | ||||
|  | ||||
|         path.xy2-y { | ||||
|             stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba | ||||
|             opacity: $opacityPlotHash !important; | ||||
|   | ||||
| @@ -21,72 +21,62 @@ | ||||
| --> | ||||
| <template> | ||||
| <div class="u-contents"> | ||||
|     <ul v-if="canEdit" | ||||
|         class="l-inspector-part" | ||||
|     <div v-if="canEdit" | ||||
|          class="grid-row" | ||||
|     > | ||||
|         <h2 v-if="heading" | ||||
|             :title="heading" | ||||
|         >{{ heading }}</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  :title="editTitle" | ||||
|             >{{ shortLabel }}</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <div class="c-click-swatch c-click-swatch--menu" | ||||
|                      @click="toggleSwatch()" | ||||
|         <div class="grid-cell label" | ||||
|              :title="editTitle" | ||||
|         >{{ shortLabel }}</div> | ||||
|         <div class="grid-cell value"> | ||||
|             <div class="c-click-swatch c-click-swatch--menu" | ||||
|                  @click="toggleSwatch()" | ||||
|             > | ||||
|                 <span class="c-color-swatch" | ||||
|                       :style="{ background: currentColor }" | ||||
|                 > | ||||
|                     <span class="c-color-swatch" | ||||
|                           :style="{ background: currentColor }" | ||||
|                 </span> | ||||
|             </div> | ||||
|             <div class="c-palette c-palette--color"> | ||||
|                 <div v-show="swatchActive" | ||||
|                      class="c-palette__items" | ||||
|                 > | ||||
|                     <div v-for="group in colorPaletteGroups" | ||||
|                          :key="group.id" | ||||
|                          class="u-contents" | ||||
|                     > | ||||
|                     </span> | ||||
|                 </div> | ||||
|                 <div class="c-palette c-palette--color"> | ||||
|                     <div v-show="swatchActive" | ||||
|                          class="c-palette__items" | ||||
|                     > | ||||
|                         <div v-for="group in colorPaletteGroups" | ||||
|                              :key="group.id" | ||||
|                              class="u-contents" | ||||
|                         <div v-for="color in group" | ||||
|                              :key="color.id" | ||||
|                              class="c-palette__item" | ||||
|                              :class="{ 'selected': currentColor === color.hexString }" | ||||
|                              :style="{ background: color.hexString }" | ||||
|                              @click="setColor(color)" | ||||
|                         > | ||||
|                             <div v-for="color in group" | ||||
|                                  :key="color.id" | ||||
|                                  class="c-palette__item" | ||||
|                                  :class="{ 'selected': currentColor === color.hexString }" | ||||
|                                  :style="{ background: color.hexString }" | ||||
|                                  @click="setColor(color)" | ||||
|                             > | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <ul v-else | ||||
|         class="l-inspector-part" | ||||
|         </div> | ||||
|     </div> | ||||
|     <div v-else | ||||
|          class="grid-row" | ||||
|     > | ||||
|         <h2 v-if="heading" | ||||
|             :title="heading" | ||||
|         >{{ heading }}</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  :title="viewTitle" | ||||
|             >{{ shortLabel }}</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <span class="c-color-swatch" | ||||
|                       :style="{ | ||||
|                           'background': currentColor | ||||
|                       }" | ||||
|                 > | ||||
|                 </span> | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|         <div class="grid-cell label" | ||||
|              :title="viewTitle" | ||||
|         >{{ shortLabel }}</div> | ||||
|         <div class="grid-cell value"> | ||||
|             <span class="c-color-swatch" | ||||
|                   :style="{ | ||||
|                       'background': currentColor | ||||
|                   }" | ||||
|             > | ||||
|             </span> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ColorPalette from './lib/ColorPalette'; | ||||
| import ColorPalette from './ColorPalette'; | ||||
| 
 | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
| @@ -114,12 +104,6 @@ export default { | ||||
|             default() { | ||||
|                 return 'Color'; | ||||
|             } | ||||
|         }, | ||||
|         heading: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -160,7 +160,9 @@ export default { | ||||
|         this.status = this.openmct.status.get(this.domainObject.identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus); | ||||
|         const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0]; | ||||
|         this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath); | ||||
|         if (provider) { | ||||
|             this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath); | ||||
|         } | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.removeStatusListener(); | ||||
| @@ -193,8 +195,10 @@ export default { | ||||
|         }, | ||||
|         showMenuItems(event) { | ||||
|             const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); | ||||
|             const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|             if (sortedActions.length) { | ||||
|                 const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view); | ||||
|                 this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|             } | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|   | ||||
| @@ -126,6 +126,11 @@ export default { | ||||
|                     this.releaseEditModeHandler(); | ||||
|                     delete this.releaseEditModeHandler; | ||||
|                 } | ||||
|  | ||||
|                 if (this.styleRuleManager) { | ||||
|                     this.styleRuleManager.destroy(); | ||||
|                     delete this.styleRuleManager; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             delete this.viewContainer; | ||||
|   | ||||
| @@ -49,6 +49,7 @@ describe("the inspector", () => { | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
| @@ -77,12 +78,12 @@ describe("the inspector", () => { | ||||
|         expect(savedStylesViewComponent.$children[0].$children.length).toBe(0); | ||||
|         stylesViewComponent.$children[0].saveStyle(mockStyle); | ||||
|  | ||||
|         stylesViewComponent.$nextTick().then(() => { | ||||
|         return stylesViewComponent.$nextTick().then(() => { | ||||
|             expect(savedStylesViewComponent.$children[0].$children.length).toBe(1); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it("should allow a saved style to be applied", () => { | ||||
|     xit("should allow a saved style to be applied", () => { | ||||
|         spyOn(openmct.editor, 'isEditing').and.returnValue(true); | ||||
|  | ||||
|         selection = mockTelemetryTableSelection; | ||||
| @@ -91,12 +92,12 @@ describe("the inspector", () => { | ||||
|  | ||||
|         stylesViewComponent.$children[0].saveStyle(mockStyle); | ||||
|  | ||||
|         stylesViewComponent.$nextTick().then(() => { | ||||
|         return stylesViewComponent.$nextTick().then(() => { | ||||
|             const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0]; | ||||
|  | ||||
|             styleSelectorComponent.selectStyle(); | ||||
|  | ||||
|             savedStylesViewComponent.$nextTick().then(() => { | ||||
|             return savedStylesViewComponent.$nextTick().then(() => { | ||||
|                 const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; | ||||
|                 const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; | ||||
|                 const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color); | ||||
| @@ -147,7 +148,7 @@ describe("the inspector", () => { | ||||
|         stylesViewComponent = createViewComponent(StylesView, selection, openmct); | ||||
|         savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); | ||||
|  | ||||
|         stylesViewComponent.$nextTick().then(() => { | ||||
|         return stylesViewComponent.$nextTick().then(() => { | ||||
|             const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; | ||||
|             const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; | ||||
|             const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; | ||||
| @@ -168,7 +169,7 @@ describe("the inspector", () => { | ||||
|         stylesViewComponent = createViewComponent(StylesView, selection, openmct); | ||||
|         savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); | ||||
|  | ||||
|         stylesViewComponent.$nextTick().then(() => { | ||||
|         return stylesViewComponent.$nextTick().then(() => { | ||||
|             const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; | ||||
|             const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; | ||||
|             const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; | ||||
| @@ -185,7 +186,7 @@ describe("the inspector", () => { | ||||
|         stylesViewComponent = createViewComponent(StylesView, selection, openmct); | ||||
|         savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); | ||||
|  | ||||
|         stylesViewComponent.$nextTick().then(() => { | ||||
|         return stylesViewComponent.$nextTick().then(() => { | ||||
|             const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; | ||||
|             const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; | ||||
|             const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; | ||||
|   | ||||
| @@ -46,6 +46,14 @@ export default { | ||||
|         'openmct', | ||||
|         'objectPath' | ||||
|     ], | ||||
|     props: { | ||||
|         viewOptions: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let domainObject = this.objectPath[0]; | ||||
|  | ||||
| @@ -109,7 +117,7 @@ export default { | ||||
|             this.view = this.currentView.view(this.domainObject, this.objectPath); | ||||
|  | ||||
|             this.getActionsCollection(); | ||||
|             this.view.show(this.viewContainer, false); | ||||
|             this.view.show(this.viewContainer, false, this.viewOptions); | ||||
|             this.initObjectStyles(); | ||||
|         }, | ||||
|         getActionsCollection() { | ||||
|   | ||||
| @@ -44,7 +44,7 @@ export default class PreviewAction { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath) { | ||||
|     invoke(objectPath, viewOptions) { | ||||
|         let preview = new Vue({ | ||||
|             components: { | ||||
|                 Preview | ||||
| @@ -53,7 +53,12 @@ export default class PreviewAction { | ||||
|                 openmct: this._openmct, | ||||
|                 objectPath: objectPath | ||||
|             }, | ||||
|             template: '<Preview></Preview>' | ||||
|             data() { | ||||
|                 return { | ||||
|                     viewOptions | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<Preview :view-options="viewOptions"></Preview>' | ||||
|         }); | ||||
|         preview.$mount(); | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,8 @@ | ||||
|         <div | ||||
|             class="l-browse-bar__object-name--w c-object-label" | ||||
|         > | ||||
|             <div class="c-object-label__type-icon" | ||||
|             <div v-if="type" | ||||
|                  class="c-object-label__type-icon" | ||||
|                  :class="type.definition.cssClass" | ||||
|             ></div> | ||||
|             <span class="l-browse-bar__object-name c-object-label__name"> | ||||
|   | ||||
| @@ -90,9 +90,9 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|         provider.view = (domainObject, objectPath) => { | ||||
|             const viewObject = wrappedView(domainObject, objectPath); | ||||
|             const wrappedShow = viewObject.show.bind(viewObject); | ||||
|             viewObject.show = (element, isEditing) => { | ||||
|             viewObject.show = (element, isEditing, viewOptions) => { | ||||
|                 viewObject.parentElement = element.parentElement; | ||||
|                 wrappedShow(element, isEditing); | ||||
|                 wrappedShow(element, isEditing, viewOptions); | ||||
|             }; | ||||
|  | ||||
|             return viewObject; | ||||
|   | ||||
| @@ -126,7 +126,7 @@ class ApplicationRouter extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Navgate to given hash and update current location object and notify listeners about location change | ||||
|      * Navigate to given hash and update current location object and notify listeners about location change | ||||
|      * | ||||
|      * @param {string} paramName name of searchParam to get from current url searchParams | ||||
|      * | ||||
| @@ -136,6 +136,13 @@ class ApplicationRouter extends EventEmitter { | ||||
|         this.handleLocationChange(hash.substring(1)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if a given object and current location object are same | ||||
|      * | ||||
|      * @param {Array<Object>} objectPath Object path of a given Domain Object | ||||
|      * | ||||
|      * @returns {Boolean} | ||||
|      */ | ||||
|     isNavigatedObject(objectPath) { | ||||
|         let targetObject = objectPath[0]; | ||||
|         let navigatedObject = this.path[0]; | ||||
|   | ||||
| @@ -42,6 +42,8 @@ const webpackConfig = { | ||||
|             "csv": "comma-separated-values", | ||||
|             "EventEmitter": "eventemitter3", | ||||
|             "bourbon": "bourbon.scss", | ||||
|             "plotly-basic": "plotly.js-basic-dist", | ||||
|             "plotly-gl2d": "plotly.js-gl2d-dist", | ||||
|             "vue": vueFile, | ||||
|             "d3-scale": path.join(__dirname, "node_modules/d3-scale/build/d3-scale.min.js"), | ||||
|             "printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user