Compare commits
	
		
			19 Commits
		
	
	
		
			change-met
			...
			tree-searc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6b719259e3 | ||
|   | 9fc31809f6 | ||
|   | ccd4bbd279 | ||
|   | 72848849dd | ||
|   | 66130ba542 | ||
|   | 895bdc164f | ||
|   | c9728144a5 | ||
|   | 76fec7f3bc | ||
|   | 1b4717065a | ||
|   | e24542c1a6 | ||
|   | b08f3106ed | ||
|   | 700bc7616d | ||
|   | 3436e976cf | ||
|   | b8e232831e | ||
|   | f6bc49fc82 | ||
|   | 7018c217c4 | ||
|   | 18c230c0f7 | ||
|   | b8c2f3f49a | ||
|   | e5e27ea498 | 
							
								
								
									
										20
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.eslintrc.js
									
									
									
									
									
								
							| @@ -54,7 +54,7 @@ module.exports = { | ||||
|             { | ||||
|                 "anonymous": "always", | ||||
|                 "asyncArrow": "always", | ||||
|                 "named": "never" | ||||
|                 "named": "never", | ||||
|             } | ||||
|         ], | ||||
|         "array-bracket-spacing": "error", | ||||
| @@ -178,10 +178,7 @@ module.exports = { | ||||
|         //https://eslint.org/docs/rules/no-whitespace-before-property | ||||
|         "no-whitespace-before-property": "error", | ||||
|         // https://eslint.org/docs/rules/object-curly-newline | ||||
|         "object-curly-newline": ["error", { | ||||
|             "consistent": true, | ||||
|             "multiline": true | ||||
|         }], | ||||
|         "object-curly-newline": ["error", {"consistent": true, "multiline": true}], | ||||
|         // https://eslint.org/docs/rules/object-property-newline | ||||
|         "object-property-newline": "error", | ||||
|         // https://eslint.org/docs/rules/brace-style | ||||
| @@ -191,7 +188,7 @@ module.exports = { | ||||
|         // https://eslint.org/docs/rules/operator-linebreak | ||||
|         "operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}], | ||||
|         // https://eslint.org/docs/rules/padding-line-between-statements | ||||
|         "padding-line-between-statements": ["error", { | ||||
|         "padding-line-between-statements":["error", { | ||||
|             "blankLine": "always", | ||||
|             "prev": "multiline-block-like", | ||||
|             "next": "*" | ||||
| @@ -203,17 +200,11 @@ module.exports = { | ||||
|         // https://eslint.org/docs/rules/space-infix-ops | ||||
|         "space-infix-ops": "error", | ||||
|         // https://eslint.org/docs/rules/space-unary-ops | ||||
|         "space-unary-ops": ["error", { | ||||
|             "words": true, | ||||
|             "nonwords": false | ||||
|         }], | ||||
|         "space-unary-ops": ["error", {"words": true, "nonwords": false}], | ||||
|         // https://eslint.org/docs/rules/arrow-spacing | ||||
|         "arrow-spacing": "error", | ||||
|         // https://eslint.org/docs/rules/semi-spacing | ||||
|         "semi-spacing": ["error", { | ||||
|             "before": false, | ||||
|             "after": true | ||||
|         }], | ||||
|         "semi-spacing": ["error", {"before": false, "after": true}], | ||||
|  | ||||
|         "vue/html-indent": [ | ||||
|             "error", | ||||
| @@ -246,7 +237,6 @@ module.exports = { | ||||
|         }], | ||||
|         "vue/multiline-html-element-content-newline": "off", | ||||
|         "vue/singleline-html-element-content-newline": "off", | ||||
|         "vue/no-mutating-props": "off" | ||||
|  | ||||
|     }, | ||||
|     "overrides": [ | ||||
|   | ||||
| @@ -138,7 +138,7 @@ define([ | ||||
|                     "id": "styleguide:home", | ||||
|                     "priority": "preferred", | ||||
|                     "model": { | ||||
|                         "type": "noneditable.folder", | ||||
|                         "type": "folder", | ||||
|                         "name": "Style Guide Home", | ||||
|                         "location": "ROOT", | ||||
|                         "composition": [ | ||||
| @@ -155,7 +155,7 @@ define([ | ||||
|                     "id": "styleguide:ui-elements", | ||||
|                     "priority": "preferred", | ||||
|                     "model": { | ||||
|                         "type": "noneditable.folder", | ||||
|                         "type": "folder", | ||||
|                         "name": "UI Elements", | ||||
|                         "location": "styleguide:home", | ||||
|                         "composition": [ | ||||
|   | ||||
| @@ -86,9 +86,7 @@ | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.PlanLayout()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.PlotVue()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
|   | ||||
| @@ -86,7 +86,7 @@ module.exports = (config) => { | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 66 | ||||
|                     lines: 65 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.6.2-SNAPSHOT", | ||||
|   "version": "1.5.0-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
| @@ -23,7 +23,7 @@ | ||||
|     "d3-time": "1.0.x", | ||||
|     "d3-time-format": "2.1.x", | ||||
|     "eslint": "7.0.0", | ||||
|     "eslint-plugin-vue": "^7.5.0", | ||||
|     "eslint-plugin-vue": "^6.0.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", | ||||
|     "eventemitter3": "^1.2.0", | ||||
|     "exports-loader": "^0.7.0", | ||||
|   | ||||
| @@ -71,10 +71,10 @@ define( | ||||
|                 openmct.editor.cancel(); | ||||
|             } | ||||
|  | ||||
|             function isFirstViewEditable(domainObject, objectPath) { | ||||
|                 let firstView = openmct.objectViews.get(domainObject, objectPath)[0]; | ||||
|             function isFirstViewEditable(domainObject) { | ||||
|                 let firstView = openmct.objectViews.get(domainObject)[0]; | ||||
|  | ||||
|                 return firstView && firstView.canEdit && firstView.canEdit(domainObject, objectPath); | ||||
|                 return firstView && firstView.canEdit && firstView.canEdit(domainObject); | ||||
|             } | ||||
|  | ||||
|             function navigateAndEdit(object) { | ||||
| @@ -88,7 +88,7 @@ define( | ||||
|  | ||||
|                 window.location.href = url; | ||||
|  | ||||
|                 if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) { | ||||
|                 if (isFirstViewEditable(object.useCapability('adapter'))) { | ||||
|                     openmct.editor.edit(); | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -44,9 +44,9 @@ define( | ||||
|             // is also invoked during the create process which should be allowed, | ||||
|             // because it may be saved elsewhere | ||||
|             if ((key === 'edit' && category === 'view-control') || key === 'properties') { | ||||
|                 let identifier = this.openmct.objects.parseKeyString(domainObject.getId()); | ||||
|                 let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId()); | ||||
|  | ||||
|                 return this.openmct.objects.isPersistable(identifier); | ||||
|                 return this.openmct.objects.isPersistable(newStyleObject); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|   | ||||
| @@ -43,8 +43,7 @@ define( | ||||
|                 ); | ||||
|  | ||||
|                 mockObjectAPI = jasmine.createSpyObj('objectAPI', [ | ||||
|                     'isPersistable', | ||||
|                     'parseKeyString' | ||||
|                     'isPersistable' | ||||
|                 ]); | ||||
|  | ||||
|                 mockAPI = { | ||||
|   | ||||
| @@ -48,9 +48,9 @@ define( | ||||
|             // prevents editing of objects that cannot be persisted, so we can assume that this | ||||
|             // is a new object. | ||||
|             if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) { | ||||
|                 let identifier = this.openmct.objects.parseKeyString(parent.getId()); | ||||
|                 let newStyleObject = objectUtils.toNewFormat(parent, parent.getId()); | ||||
|  | ||||
|                 return this.openmct.objects.isPersistable(identifier); | ||||
|                 return this.openmct.objects.isPersistable(newStyleObject); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|   | ||||
| @@ -33,8 +33,7 @@ define( | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 objectAPI = jasmine.createSpyObj('objectsAPI', [ | ||||
|                     'isPersistable', | ||||
|                     'parseKeyString' | ||||
|                     'isPersistable' | ||||
|                 ]); | ||||
|  | ||||
|                 mockOpenMCT = { | ||||
|   | ||||
| @@ -37,7 +37,7 @@ define( | ||||
|             this.$q = $q; | ||||
|         } | ||||
|  | ||||
|         LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) { | ||||
|         LocatingObjectDecorator.prototype.getObjects = function (ids) { | ||||
|             var $q = this.$q, | ||||
|                 $log = this.$log, | ||||
|                 objectService = this.objectService, | ||||
| @@ -79,7 +79,7 @@ define( | ||||
|                         }); | ||||
|                 } | ||||
|  | ||||
|                 return objectService.getObjects([id], abortSignal).then(attachContext); | ||||
|                 return objectService.getObjects([id]).then(attachContext); | ||||
|             } | ||||
|  | ||||
|             ids.forEach(function (id) { | ||||
|   | ||||
| @@ -130,9 +130,12 @@ define([ | ||||
|         mutationTopic.listen(mutatedObject => { | ||||
|             let editor = mutatedObject.getCapability('editor'); | ||||
|             if (!editor || !editor.inEditContext()) { | ||||
|                 let mutatedObjectModel = mutatedObject.getModel(); | ||||
|                 this.index( | ||||
|                     mutatedObject.getId(), | ||||
|                     mutatedObject.getModel() | ||||
|                     mutatedObjectModel, | ||||
|                     mutatedObjectModel.type | ||||
|  | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
| @@ -146,15 +149,10 @@ define([ | ||||
|      * @param {String} id to be indexed. | ||||
|      */ | ||||
|     GenericSearchProvider.prototype.scheduleForIndexing = function (id) { | ||||
|         const identifier = objectUtils.parseKeyString(id); | ||||
|         const objectProvider = this.openmct.objects.getProvider(identifier); | ||||
|  | ||||
|         if (objectProvider === undefined || objectProvider.search === undefined) { | ||||
|             if (!this.indexedIds[id] && !this.pendingIndex[id]) { | ||||
|                 this.indexedIds[id] = true; | ||||
|                 this.pendingIndex[id] = true; | ||||
|                 this.idsToIndex.push(id); | ||||
|             } | ||||
|         if (!this.indexedIds[id] && !this.pendingIndex[id]) { | ||||
|             this.indexedIds[id] = true; | ||||
|             this.pendingIndex[id] = true; | ||||
|             this.idsToIndex.push(id); | ||||
|         } | ||||
|  | ||||
|         this.keepIndexing(); | ||||
| @@ -182,14 +180,15 @@ define([ | ||||
|      * @param id a model id | ||||
|      * @param model a model | ||||
|      */ | ||||
|     GenericSearchProvider.prototype.index = function (id, model) { | ||||
|     GenericSearchProvider.prototype.index = function (id, model, type) { | ||||
|         var provider = this; | ||||
|  | ||||
|         if (id !== 'ROOT') { | ||||
|             this.worker.postMessage({ | ||||
|                 request: 'index', | ||||
|                 model: model, | ||||
|                 id: id | ||||
|                 id: id, | ||||
|                 type: type | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @@ -227,7 +226,7 @@ define([ | ||||
|             .then(function (objects) { | ||||
|                 delete provider.pendingIndex[idToIndex]; | ||||
|                 if (objects[idToIndex]) { | ||||
|                     provider.index(idToIndex, objects[idToIndex].model); | ||||
|                     provider.index(idToIndex, objects[idToIndex].model, objects[idToIndex].model.type); | ||||
|                 } | ||||
|             }, function () { | ||||
|                 provider | ||||
| @@ -278,7 +277,9 @@ define([ | ||||
|  | ||||
|             modelResults.hits = event.data.results.map(function (hit) { | ||||
|                 return { | ||||
|                     id: hit.id | ||||
|                     id: hit.id, | ||||
|                     name: hit.name, | ||||
|                     type: hit.type | ||||
|                 }; | ||||
|             }); | ||||
|         } | ||||
|   | ||||
| @@ -80,15 +80,13 @@ define([ | ||||
|      * @param {Function} [filter] if provided, will be called for every | ||||
|      *   potential modelResult.  If it returns false, the model result will be | ||||
|      *   excluded from the search results. | ||||
|      * @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any | ||||
|      *   downstream fetch requests. | ||||
|      * @returns {Promise} A Promise for a search result object. | ||||
|      */ | ||||
|     SearchAggregator.prototype.query = function ( | ||||
|         inputText, | ||||
|         maxResults, | ||||
|         filter, | ||||
|         abortSignal | ||||
|         indexOnly | ||||
|     ) { | ||||
|  | ||||
|         var aggregator = this, | ||||
| @@ -123,10 +121,24 @@ define([ | ||||
|                 modelResults = aggregator.applyFilter(modelResults, filter); | ||||
|                 modelResults = aggregator.removeDuplicates(modelResults); | ||||
|  | ||||
|                 return aggregator.asObjectResults(modelResults, abortSignal); | ||||
|                 if (indexOnly) { | ||||
|                     return Promise.resolve(modelResults); | ||||
|                 } | ||||
|  | ||||
|                 return aggregator.asObjectResults(modelResults); | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     SearchAggregator.prototype.queryLite = function ( | ||||
|         inputText, | ||||
|         maxResults, | ||||
|         filter | ||||
|     ) { | ||||
|         const INDEX_ONLY = true; | ||||
|  | ||||
|         return this.query(inputText, maxResults, filter, INDEX_ONLY); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Order model results by score descending and return them. | ||||
|      */ | ||||
| @@ -196,19 +208,16 @@ define([ | ||||
|      * Convert modelResults to objectResults by fetching them from the object | ||||
|      * service. | ||||
|      * | ||||
|      * @param {Object} modelResults an object containing the results from the search | ||||
|      * @param {AbortController.signal} abortSignal (optional) abort signal to cancel any | ||||
|      *   downstream fetch requests | ||||
|      * @returns {Promise} for an objectResults object. | ||||
|      */ | ||||
|     SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) { | ||||
|     SearchAggregator.prototype.asObjectResults = function (modelResults) { | ||||
|         var objectIds = modelResults.hits.map(function (modelResult) { | ||||
|             return modelResult.id; | ||||
|         }); | ||||
|  | ||||
|         return this | ||||
|             .objectService | ||||
|             .getObjects(objectIds, abortSignal) | ||||
|             .getObjects(objectIds) | ||||
|             .then(function (objects) { | ||||
|  | ||||
|                 var objectResults = { | ||||
|   | ||||
| @@ -219,7 +219,7 @@ define([ | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objects | ||||
|          */ | ||||
|         this.objects = new api.ObjectAPI.default(this.types, this); | ||||
|         this.objects = new api.ObjectAPI(); | ||||
|  | ||||
|         /** | ||||
|          * An interface for retrieving and interpreting telemetry data associated | ||||
| @@ -283,7 +283,6 @@ define([ | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.NonEditableFolder()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
| @@ -372,7 +371,7 @@ define([ | ||||
|      *        MCT; if undefined, MCT will be run in the body of the document | ||||
|      */ | ||||
|     MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) { | ||||
|         if (this.types.get('layout') === undefined) { | ||||
|         if (!this.plugins.DisplayLayout._installed) { | ||||
|             this.install(this.plugins.DisplayLayout({ | ||||
|                 showAsView: ['summary-widget'] | ||||
|             })); | ||||
|   | ||||
| @@ -32,7 +32,7 @@ define([], function () { | ||||
|         if (Object.prototype.hasOwnProperty.call(view, 'provider')) { | ||||
|             const domainObject = legacyObject.useCapability('adapter'); | ||||
|  | ||||
|             return view.provider.canView(domainObject, this.openmct.router.path); | ||||
|             return view.provider.canView(domainObject); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|   | ||||
| @@ -61,7 +61,6 @@ define([ | ||||
|             const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId()); | ||||
|             const keystring = utils.makeKeyString(newStyleObject.identifier); | ||||
|  | ||||
|             this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject); | ||||
|             this.eventEmitter.emit(keystring + ":*", newStyleObject); | ||||
|             this.eventEmitter.emit('mutation', newStyleObject); | ||||
|         }.bind(this); | ||||
| @@ -139,26 +138,18 @@ define([ | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) { | ||||
|         const searchService = this.$injector.get('searchService'); | ||||
|  | ||||
|         // need to pass the abortSignal down, so need to | ||||
|         // pass in undefined for maxResults and filter on query | ||||
|         return searchService.query(query, undefined, undefined, abortSignal); | ||||
|     }; | ||||
|  | ||||
|     // Injects new object API as a decorator so that it hijacks all requests. | ||||
|     // Object providers implemented on new API should just work, old API should just work, many things may break. | ||||
|     function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) { | ||||
|         const eventEmitter = openmct.objects.eventEmitter; | ||||
|  | ||||
|         this.getObjects = function (keys, abortSignal) { | ||||
|         this.getObjects = function (keys) { | ||||
|             const results = {}; | ||||
|  | ||||
|             const promises = keys.map(function (keyString) { | ||||
|                 const key = utils.parseKeyString(keyString); | ||||
|  | ||||
|                 return openmct.objects.get(key, abortSignal) | ||||
|                 return openmct.objects.get(key) | ||||
|                     .then(function (object) { | ||||
|                         object = utils.toOldFormat(object); | ||||
|                         results[keyString] = instantiate(object, keyString); | ||||
|   | ||||
| @@ -29,22 +29,9 @@ describe('The ActionCollection', () => { | ||||
|     let mockApplicableActions; | ||||
|     let mockObjectPath; | ||||
|     let mockView; | ||||
|     let mockIdentifierService; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.$injector = jasmine.createSpyObj('$injector', ['get']); | ||||
|         mockIdentifierService = jasmine.createSpyObj( | ||||
|             'identifierService', | ||||
|             ['parse'] | ||||
|         ); | ||||
|         mockIdentifierService.parse.and.returnValue({ | ||||
|             getSpace: () => { | ||||
|                 return ''; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         openmct.$injector.get.and.returnValue(mockIdentifierService); | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
| @@ -63,10 +50,6 @@ describe('The ActionCollection', () => { | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [ | ||||
|             'create', | ||||
|             'update' | ||||
|         ])); | ||||
|         mockView = { | ||||
|             getViewContext: () => { | ||||
|                 return { | ||||
|   | ||||
| @@ -60,17 +60,6 @@ define([ | ||||
|         }; | ||||
|         this.onProviderAdd = this.onProviderAdd.bind(this); | ||||
|         this.onProviderRemove = this.onProviderRemove.bind(this); | ||||
|         this.mutables = {}; | ||||
|  | ||||
|         if (this.domainObject.isMutable) { | ||||
|             this.returnMutables = true; | ||||
|             let unobserve = this.domainObject.$on('$_destroy', () => { | ||||
|                 Object.values(this.mutables).forEach(mutable => { | ||||
|                     this.publicAPI.objects.destroyMutable(mutable); | ||||
|                 }); | ||||
|                 unobserve(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -86,6 +75,10 @@ define([ | ||||
|             throw new Error('Event not supported by composition: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (!this.mutationListener) { | ||||
|             this._synchronize(); | ||||
|         } | ||||
|  | ||||
|         if (this.provider.on && this.provider.off) { | ||||
|             if (event === 'add') { | ||||
|                 this.provider.on( | ||||
| @@ -196,13 +189,6 @@ define([ | ||||
|  | ||||
|             this.provider.add(this.domainObject, child.identifier); | ||||
|         } else { | ||||
|             if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) { | ||||
|                 let keyString = this.publicAPI.objects.makeKeyString(child.identifier); | ||||
|  | ||||
|                 child = this.publicAPI.objects._toMutable(child); | ||||
|                 this.mutables[keyString] = child; | ||||
|             } | ||||
|  | ||||
|             this.emit('add', child); | ||||
|         } | ||||
|     }; | ||||
| @@ -216,8 +202,6 @@ define([ | ||||
|      * @name load | ||||
|      */ | ||||
|     CompositionCollection.prototype.load = function () { | ||||
|         this.cleanUpMutables(); | ||||
|  | ||||
|         return this.provider.load(this.domainObject) | ||||
|             .then(function (children) { | ||||
|                 return Promise.all(children.map((c) => this.publicAPI.objects.get(c))); | ||||
| @@ -250,14 +234,6 @@ define([ | ||||
|         if (!skipMutate) { | ||||
|             this.provider.remove(this.domainObject, child.identifier); | ||||
|         } else { | ||||
|             if (this.returnMutables) { | ||||
|                 let keyString = this.publicAPI.objects.makeKeyString(child); | ||||
|                 if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) { | ||||
|                     this.publicAPI.objects.destroyMutable(this.mutables[keyString]); | ||||
|                     delete this.mutables[keyString]; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.emit('remove', child); | ||||
|         } | ||||
|     }; | ||||
| @@ -305,6 +281,12 @@ define([ | ||||
|         this.remove(child, true); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype._synchronize = function () { | ||||
|         this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => { | ||||
|             this.domainObject = JSON.parse(JSON.stringify(newDomainObject)); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype._destroy = function () { | ||||
|         if (this.mutationListener) { | ||||
|             this.mutationListener(); | ||||
| @@ -326,11 +308,5 @@ define([ | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype.cleanUpMutables = function () { | ||||
|         Object.values(this.mutables).forEach(mutable => { | ||||
|             this.publicAPI.objects.destroyMutable(mutable); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     return CompositionCollection; | ||||
| }); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ class MenuAPI { | ||||
|         this._showObjectMenu = this._showObjectMenu.bind(this); | ||||
|     } | ||||
|  | ||||
|     showMenu(x, y, actions, onDestroy) { | ||||
|     showMenu(x, y, actions) { | ||||
|         if (this.menuComponent) { | ||||
|             this.menuComponent.dismiss(); | ||||
|         } | ||||
| @@ -46,8 +46,7 @@ class MenuAPI { | ||||
|         let options = { | ||||
|             x, | ||||
|             y, | ||||
|             actions, | ||||
|             onDestroy | ||||
|             actions | ||||
|         }; | ||||
|  | ||||
|         this.menuComponent = new Menu(options); | ||||
|   | ||||
| @@ -31,7 +31,6 @@ describe ('The Menu API', () => { | ||||
|     let x; | ||||
|     let y; | ||||
|     let result; | ||||
|     let onDestroy; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
| @@ -74,9 +73,7 @@ describe ('The Menu API', () => { | ||||
|             let vueComponent; | ||||
|  | ||||
|             beforeEach(() => { | ||||
|                 onDestroy = jasmine.createSpy('onDestroy'); | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, onDestroy); | ||||
|                 menuAPI.showMenu(x, y, actionsArray); | ||||
|                 vueComponent = menuAPI.menuComponent.component; | ||||
|                 menuComponent = document.querySelector(".c-menu"); | ||||
|  | ||||
| @@ -123,12 +120,6 @@ describe ('The Menu API', () => { | ||||
|  | ||||
|                 expect(vueComponent.$destroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the onDestroy callback if passed in", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(onDestroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -30,12 +30,12 @@ class Menu extends EventEmitter { | ||||
|         this.options = options; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 actions: options.actions | ||||
|             }, | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -75,20 +75,13 @@ export default class NotificationAPI extends EventEmitter { | ||||
|      * Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief | ||||
|      * period of time. | ||||
|      * @param {string} message The message to display to the user | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {InfoNotification} | ||||
|      */ | ||||
|     info(message, options = {}) { | ||||
|     info(message) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             autoDismiss: true, | ||||
|             severity: "info", | ||||
|             options | ||||
|             severity: "info" | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -97,19 +90,12 @@ export default class NotificationAPI extends EventEmitter { | ||||
|     /** | ||||
|      * Present an alert to the user. | ||||
|      * @param {string} message The message to display to the user. | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {Notification} | ||||
|      */ | ||||
|     alert(message, options = {}) { | ||||
|     alert(message) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             severity: "alert", | ||||
|             options | ||||
|             severity: "alert" | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -118,19 +104,12 @@ export default class NotificationAPI extends EventEmitter { | ||||
|     /** | ||||
|      * Present an error message to the user | ||||
|      * @param {string} message | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {Notification} | ||||
|      */ | ||||
|     error(message, options = {}) { | ||||
|     error(message) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             severity: "error", | ||||
|             options | ||||
|             severity: "error" | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -346,11 +325,9 @@ export default class NotificationAPI extends EventEmitter { | ||||
|         this.emit('notification', notification); | ||||
|  | ||||
|         if (notification.model.autoDismiss || this._selectNextNotification()) { | ||||
|             const autoDismissTimeout = notification.model.options.autoDismissTimeout | ||||
|                 || DEFAULT_AUTO_DISMISS_TIMEOUT; | ||||
|             this.activeTimeout = setTimeout(() => { | ||||
|                 this._dismissOrMinimize(notification); | ||||
|             }, autoDismissTimeout); | ||||
|             }, DEFAULT_AUTO_DISMISS_TIMEOUT); | ||||
|         } else { | ||||
|             delete this.activeTimeout; | ||||
|         } | ||||
|   | ||||
| @@ -1,154 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import NotificationAPI from './NotificationAPI'; | ||||
|  | ||||
| describe('The Notifiation API', () => { | ||||
|     let notificationAPIInstance; | ||||
|     let defaultTimeout = 4000; | ||||
|  | ||||
|     beforeAll(() => { | ||||
|         notificationAPIInstance = new NotificationAPI(); | ||||
|     }); | ||||
|  | ||||
|     describe('the info method', () => { | ||||
|         let message = 'Example Notification Message'; | ||||
|         let severity = 'info'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.info(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message with info severity', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('auto dismisses the notification after a brief timeout', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(0); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the alert method', () => { | ||||
|         let message = 'Example alert message'; | ||||
|         let severity = 'alert'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.alert(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message, with alert severity', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('does not auto dismiss the notification', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(1); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the error method', () => { | ||||
|         let message = 'Example error message'; | ||||
|         let severity = 'error'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.error(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message, with severity error', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('does not auto dismiss the notification', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(1); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the progress method', () => { | ||||
|         let title = 'This is a progress notification'; | ||||
|         let message1 = 'Example progress message 1'; | ||||
|         let message2 = 'Example progress message 2'; | ||||
|         let percentage1 = 50; | ||||
|         let percentage2 = 99.9; | ||||
|         let severity = 'info'; | ||||
|         let notification; | ||||
|         let updatedPercentage; | ||||
|         let updatedMessage; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notification = notificationAPIInstance.progress(title, percentage1, message1); | ||||
|             notification.on('progress', (percentage, text) => { | ||||
|                 updatedPercentage = percentage; | ||||
|                 updatedMessage = text; | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it ('shows a notification with a message, progress message, percentage and info severity', () => { | ||||
|             expect(notification.model.message).toEqual(title); | ||||
|             expect(notification.model.severity).toEqual(severity); | ||||
|             expect(notification.model.progressText).toEqual(message1); | ||||
|             expect(notification.model.progressPerc).toEqual(percentage1); | ||||
|         }); | ||||
|  | ||||
|         it ('allows dynamically updating the progress attributes', () => { | ||||
|             notification.progress(percentage2, message2); | ||||
|  | ||||
|             expect(updatedPercentage).toEqual(percentage2); | ||||
|             expect(updatedMessage).toEqual(message2); | ||||
|         }); | ||||
|  | ||||
|         it ('allows dynamically dismissing of progress notification', () => { | ||||
|             notification.dismiss(); | ||||
|  | ||||
|             expect(notificationAPIInstance.notifications.length).toEqual(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,147 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import _ from 'lodash'; | ||||
| import utils from './object-utils.js'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| const ANY_OBJECT_EVENT = 'mutation'; | ||||
|  | ||||
| /** | ||||
|  * Wraps a domain object to keep its model synchronized with other instances of the same object. | ||||
|  * | ||||
|  * Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers | ||||
|  * should be careful to destroy MutableDomainObject in order to avoid memory leaks. | ||||
|  * | ||||
|  * All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except | ||||
|  * `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()` | ||||
|  * will result in the creation of a new MutableDomainObject and you will be responsible for destroying it | ||||
|  * (via openmct.objects.destroy) when you're done with it. | ||||
|  * | ||||
|  * @typedef MutableDomainObject | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
| class MutableDomainObject { | ||||
|     constructor(eventEmitter) { | ||||
|         Object.defineProperties(this, { | ||||
|             _globalEventEmitter: { | ||||
|                 value: eventEmitter, | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             _instanceEventEmitter: { | ||||
|                 value: new EventEmitter(), | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             _observers: { | ||||
|                 value: [], | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             isMutable: { | ||||
|                 value: true, | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     $observe(path, callback) { | ||||
|         let fullPath = qualifiedEventName(this, path); | ||||
|         let eventOff = | ||||
|             this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback); | ||||
|  | ||||
|         this._globalEventEmitter.on(fullPath, callback); | ||||
|         this._observers.push(eventOff); | ||||
|  | ||||
|         return eventOff; | ||||
|     } | ||||
|     $set(path, value) { | ||||
|         _.set(this, path, value); | ||||
|         _.set(this, 'modified', Date.now()); | ||||
|  | ||||
|         //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired. | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this); | ||||
|  | ||||
|         //Emit a general "any object" event | ||||
|         this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this); | ||||
|         //Emit wildcard event, with path so that callback knows what changed | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value); | ||||
|  | ||||
|         //Emit events specific to properties affected | ||||
|         let parentPropertiesList = path.split('.'); | ||||
|         for (let index = parentPropertiesList.length; index > 0; index--) { | ||||
|             let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); | ||||
|             this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath)); | ||||
|         } | ||||
|  | ||||
|         //TODO: Emit events for listeners of child properties when parent changes. | ||||
|         // Do it at observer time - also register observers for parent attribute path. | ||||
|     } | ||||
|  | ||||
|     $refresh(model) { | ||||
|         //TODO: Currently we are updating the entire object. | ||||
|         // In the future we could update a specific property of the object using the 'path' parameter. | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model); | ||||
|  | ||||
|         //Emit wildcard event, with path so that callback knows what changed | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this); | ||||
|     } | ||||
|  | ||||
|     $on(event, callback) { | ||||
|         this._instanceEventEmitter.on(event, callback); | ||||
|  | ||||
|         return () => this._instanceEventEmitter.off(event, callback); | ||||
|     } | ||||
|     $destroy() { | ||||
|         this._observers.forEach(observer => observer()); | ||||
|         delete this._globalEventEmitter; | ||||
|         delete this._observers; | ||||
|         this._instanceEventEmitter.emit('$_destroy'); | ||||
|     } | ||||
|  | ||||
|     static createMutable(object, mutationTopic) { | ||||
|         let mutable = Object.create(new MutableDomainObject(mutationTopic)); | ||||
|         Object.assign(mutable, object); | ||||
|  | ||||
|         mutable.$observe('$_synchronize_model', (updatedObject) => { | ||||
|             let clone = JSON.parse(JSON.stringify(updatedObject)); | ||||
|             let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject)); | ||||
|             deleted.forEach((propertyName) => delete mutable[propertyName]); | ||||
|             Object.assign(mutable, clone); | ||||
|         }); | ||||
|  | ||||
|         return mutable; | ||||
|     } | ||||
|  | ||||
|     static mutateObject(object, path, value) { | ||||
|         _.set(object, path, value); | ||||
|         _.set(object, 'modified', Date.now()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function qualifiedEventName(object, eventName) { | ||||
|     let keystring = utils.makeKeyString(object.identifier); | ||||
|  | ||||
|     return [keystring, eventName].join(':'); | ||||
| } | ||||
|  | ||||
| export default MutableDomainObject; | ||||
							
								
								
									
										102
									
								
								src/api/objects/MutableObject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/api/objects/MutableObject.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     utils, | ||||
|     _ | ||||
| ) { | ||||
|     const ANY_OBJECT_EVENT = "mutation"; | ||||
|  | ||||
|     /** | ||||
|      * The MutableObject wraps a DomainObject and provides getters and | ||||
|      * setters for | ||||
|      * @param eventEmitter | ||||
|      * @param object | ||||
|      * @interface MutableObject | ||||
|      */ | ||||
|     function MutableObject(eventEmitter, object) { | ||||
|         this.eventEmitter = eventEmitter; | ||||
|         this.object = object; | ||||
|         this.unlisteners = []; | ||||
|     } | ||||
|  | ||||
|     function qualifiedEventName(object, eventName) { | ||||
|         const keystring = utils.makeKeyString(object.identifier); | ||||
|  | ||||
|         return [keystring, eventName].join(':'); | ||||
|     } | ||||
|  | ||||
|     MutableObject.prototype.stopListening = function () { | ||||
|         this.unlisteners.forEach(function (unlisten) { | ||||
|             unlisten(); | ||||
|         }); | ||||
|         this.unlisteners = []; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Observe changes to this domain object. | ||||
|      * @param {string} path the property to observe | ||||
|      * @param {Function} callback a callback to invoke when new values for | ||||
|      *        this property are observed | ||||
|      * @method on | ||||
|      * @memberof module:openmct.MutableObject# | ||||
|      */ | ||||
|     MutableObject.prototype.on = function (path, callback) { | ||||
|         const fullPath = qualifiedEventName(this.object, path); | ||||
|         const eventOff = | ||||
|             this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback); | ||||
|  | ||||
|         this.eventEmitter.on(fullPath, callback); | ||||
|         this.unlisteners.push(eventOff); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Modify this domain object. | ||||
|      * @param {string} path the property to modify | ||||
|      * @param {*} value the new value for this property | ||||
|      * @method set | ||||
|      * @memberof module:openmct.MutableObject# | ||||
|      */ | ||||
|     MutableObject.prototype.set = function (path, value) { | ||||
|         _.set(this.object, path, value); | ||||
|         _.set(this.object, 'modified', Date.now()); | ||||
|  | ||||
|         const handleRecursiveMutation = function (newObject) { | ||||
|             this.object = newObject; | ||||
|         }.bind(this); | ||||
|  | ||||
|         //Emit wildcard event | ||||
|         this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object); | ||||
|         //Emit a general "any object" event | ||||
|         this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object); | ||||
|  | ||||
|         this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation); | ||||
|         //Emit event specific to property | ||||
|         this.eventEmitter.emit(qualifiedEventName(this.object, path), value); | ||||
|         this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation); | ||||
|     }; | ||||
|  | ||||
|     return MutableObject; | ||||
| }); | ||||
| @@ -20,508 +20,365 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import utils from 'objectUtils'; | ||||
| import MutableDomainObject from './MutableDomainObject'; | ||||
| import RootRegistry from './RootRegistry'; | ||||
| import RootObjectProvider from './RootObjectProvider'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import InterceptorRegistry from './InterceptorRegistry'; | ||||
| define([ | ||||
|     'lodash', | ||||
|     'objectUtils', | ||||
|     './MutableObject', | ||||
|     './RootRegistry', | ||||
|     './RootObjectProvider', | ||||
|     './InterceptorRegistry', | ||||
|     'EventEmitter' | ||||
| ], function ( | ||||
|     _, | ||||
|     utils, | ||||
|     MutableObject, | ||||
|     RootRegistry, | ||||
|     RootObjectProvider, | ||||
|     InterceptorRegistry, | ||||
|     EventEmitter | ||||
| ) { | ||||
|  | ||||
| /** | ||||
|  * Utilities for loading, saving, and manipulating domain objects. | ||||
|  * @interface ObjectAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|     /** | ||||
|      * Utilities for loading, saving, and manipulating domain objects. | ||||
|      * @interface ObjectAPI | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|  | ||||
| function ObjectAPI(typeRegistry, openmct) { | ||||
|     this.typeRegistry = typeRegistry; | ||||
|     this.eventEmitter = new EventEmitter(); | ||||
|     this.providers = {}; | ||||
|     this.rootRegistry = new RootRegistry(); | ||||
|     this.injectIdentifierService = function () { | ||||
|         this.identifierService = openmct.$injector.get("identifierService"); | ||||
|     function ObjectAPI() { | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.providers = {}; | ||||
|         this.rootRegistry = new RootRegistry(); | ||||
|         this.rootProvider = new RootObjectProvider.default(this.rootRegistry); | ||||
|         this.cache = {}; | ||||
|         this.interceptorRegistry = new InterceptorRegistry.default(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set fallback provider, this is an internal API for legacy reasons. | ||||
|      * @private | ||||
|      */ | ||||
|     ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { | ||||
|         this.fallbackProvider = p; | ||||
|     }; | ||||
|  | ||||
|     this.rootProvider = new RootObjectProvider(this.rootRegistry); | ||||
|     this.cache = {}; | ||||
|     this.interceptorRegistry = new InterceptorRegistry(); | ||||
| } | ||||
|     /** | ||||
|      * Retrieve the provider for a given identifier. | ||||
|      * @private | ||||
|      */ | ||||
|     ObjectAPI.prototype.getProvider = function (identifier) { | ||||
|         if (identifier.key === 'ROOT') { | ||||
|             return this.rootProvider; | ||||
|         } | ||||
|  | ||||
| /** | ||||
|  * Set fallback provider, this is an internal API for legacy reasons. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { | ||||
|     this.fallbackProvider = p; | ||||
| }; | ||||
|         return this.providers[identifier.namespace] || this.fallbackProvider; | ||||
|     }; | ||||
|  | ||||
| /** | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.getIdentifierService = function () { | ||||
|     // Lazily acquire identifier service | ||||
|     if (!this.identifierService) { | ||||
|         this.injectIdentifierService(); | ||||
|     } | ||||
|     /** | ||||
|      * Get the root-level object. | ||||
|      * @returns {Promise.<DomainObject>} a promise for the root object | ||||
|      */ | ||||
|     ObjectAPI.prototype.getRoot = function () { | ||||
|         return this.rootProvider.get(); | ||||
|     }; | ||||
|  | ||||
|     return this.identifierService; | ||||
| }; | ||||
|     /** | ||||
|      * Register a new object provider for a particular namespace. | ||||
|      * | ||||
|      * @param {string} namespace the namespace for which to provide objects | ||||
|      * @param {module:openmct.ObjectProvider} provider the provider which | ||||
|      *        will handle loading domain objects from this namespace | ||||
|      * @memberof {module:openmct.ObjectAPI#} | ||||
|      * @name addProvider | ||||
|      */ | ||||
|     ObjectAPI.prototype.addProvider = function (namespace, provider) { | ||||
|         this.providers[namespace] = provider; | ||||
|     }; | ||||
|  | ||||
| /** | ||||
|  * Retrieve the provider for a given identifier. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.getProvider = function (identifier) { | ||||
|     //handles the '' vs 'mct' namespace issue | ||||
|     const keyString = utils.makeKeyString(identifier); | ||||
|     const identifierService = this.getIdentifierService(); | ||||
|     const namespace = identifierService.parse(keyString).getSpace(); | ||||
|     /** | ||||
|      * Provides the ability to read, write, and delete domain objects. | ||||
|      * | ||||
|      * When registering a new object provider, all methods on this interface | ||||
|      * are optional. | ||||
|      * | ||||
|      * @interface ObjectProvider | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|  | ||||
|     if (identifier.key === 'ROOT') { | ||||
|         return this.rootProvider; | ||||
|     } | ||||
|     /** | ||||
|      * Create the given domain object in the corresponding persistence store | ||||
|      * | ||||
|      * @method create | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        create | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been created, or be rejected if it cannot be saved | ||||
|      */ | ||||
|  | ||||
|     return this.providers[namespace] || this.fallbackProvider; | ||||
| }; | ||||
|     /** | ||||
|      * Update this domain object in its persistence store | ||||
|      * | ||||
|      * @method update | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        update | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been updated, or be rejected if it cannot be saved | ||||
|      */ | ||||
|  | ||||
| /** | ||||
|  * Get the root-level object. | ||||
|  * @returns {Promise.<DomainObject>} a promise for the root object | ||||
|  */ | ||||
| ObjectAPI.prototype.getRoot = function () { | ||||
|     return this.rootProvider.get(); | ||||
| }; | ||||
|     /** | ||||
|      * Delete this domain object. | ||||
|      * | ||||
|      * @method delete | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        delete | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been deleted, or be rejected if it cannot be deleted | ||||
|      */ | ||||
|  | ||||
| /** | ||||
|  * Register a new object provider for a particular namespace. | ||||
|  * | ||||
|  * @param {string} namespace the namespace for which to provide objects | ||||
|  * @param {module:openmct.ObjectProvider} provider the provider which | ||||
|  *        will handle loading domain objects from this namespace | ||||
|  * @memberof {module:openmct.ObjectAPI#} | ||||
|  * @name addProvider | ||||
|  */ | ||||
| ObjectAPI.prototype.addProvider = function (namespace, provider) { | ||||
|     this.providers[namespace] = provider; | ||||
| }; | ||||
|     /** | ||||
|      * Get a domain object. | ||||
|      * | ||||
|      * @method get | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {string} key the key for the domain object to load | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|  | ||||
| /** | ||||
|  * Provides the ability to read, write, and delete domain objects. | ||||
|  * | ||||
|  * When registering a new object provider, all methods on this interface | ||||
|  * are optional. | ||||
|  * | ||||
|  * @interface ObjectProvider | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|     /** | ||||
|      * Get a domain object. | ||||
|      * | ||||
|      * @method get | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|      *        the identifier for the domain object to load | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     ObjectAPI.prototype.get = function (identifier) { | ||||
|         let keystring = this.makeKeyString(identifier); | ||||
|         if (this.cache[keystring] !== undefined) { | ||||
|             return this.cache[keystring]; | ||||
|         } | ||||
|  | ||||
| /** | ||||
|  * Create the given domain object in the corresponding persistence store | ||||
|  * | ||||
|  * @method create | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        create | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been created, or be rejected if it cannot be saved | ||||
|  */ | ||||
|         identifier = utils.parseKeyString(identifier); | ||||
|         const provider = this.getProvider(identifier); | ||||
|  | ||||
| /** | ||||
|  * Update this domain object in its persistence store | ||||
|  * | ||||
|  * @method update | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        update | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been updated, or be rejected if it cannot be saved | ||||
|  */ | ||||
|         if (!provider) { | ||||
|             throw new Error('No Provider Matched'); | ||||
|         } | ||||
|  | ||||
| /** | ||||
|  * Delete this domain object. | ||||
|  * | ||||
|  * @method delete | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        delete | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been deleted, or be rejected if it cannot be deleted | ||||
|  */ | ||||
|         if (!provider.get) { | ||||
|             throw new Error('Provider does not support get!'); | ||||
|         } | ||||
|  | ||||
| /** | ||||
|  * Get a domain object. | ||||
|  * | ||||
|  * @method get | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {string} key the key for the domain object to load | ||||
|  * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been saved, or be rejected if it cannot be saved | ||||
|  */ | ||||
|         let objectPromise = provider.get(identifier); | ||||
|  | ||||
| ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|     let keystring = this.makeKeyString(identifier); | ||||
|     if (this.cache[keystring] !== undefined) { | ||||
|         return this.cache[keystring]; | ||||
|     } | ||||
|         this.cache[keystring] = objectPromise; | ||||
|  | ||||
|     identifier = utils.parseKeyString(identifier); | ||||
|     const provider = this.getProvider(identifier); | ||||
|         return objectPromise.then(result => { | ||||
|             delete this.cache[keystring]; | ||||
|             const interceptors = this.listGetInterceptors(identifier, result); | ||||
|             interceptors.forEach(interceptor => { | ||||
|                 result = interceptor.invoke(identifier, result); | ||||
|             }); | ||||
|  | ||||
|     if (!provider) { | ||||
|         throw new Error('No Provider Matched'); | ||||
|     } | ||||
|             return result; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     if (!provider.get) { | ||||
|         throw new Error('Provider does not support get!'); | ||||
|     } | ||||
|     ObjectAPI.prototype.delete = function () { | ||||
|         throw new Error('Delete not implemented'); | ||||
|     }; | ||||
|  | ||||
|     let objectPromise = provider.get(identifier, abortSignal); | ||||
|     this.cache[keystring] = objectPromise; | ||||
|     ObjectAPI.prototype.isPersistable = function (domainObject) { | ||||
|         let provider = this.getProvider(domainObject.identifier); | ||||
|  | ||||
|     return objectPromise.then(result => { | ||||
|         delete this.cache[keystring]; | ||||
|         result = this.applyGetInterceptors(identifier, result); | ||||
|         return provider !== undefined | ||||
|             && provider.create !== undefined | ||||
|             && provider.update !== undefined; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Save this domain object in its current state. EXPERIMENTAL | ||||
|      * | ||||
|      * @private | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        save | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     ObjectAPI.prototype.save = function (domainObject) { | ||||
|         let provider = this.getProvider(domainObject.identifier); | ||||
|         let savedResolve; | ||||
|         let result; | ||||
|  | ||||
|         if (!this.isPersistable(domainObject)) { | ||||
|             result = Promise.reject('Object provider does not support saving'); | ||||
|         } else if (hasAlreadyBeenPersisted(domainObject)) { | ||||
|             result = Promise.resolve(true); | ||||
|         } else { | ||||
|             const persistedTime = Date.now(); | ||||
|             if (domainObject.persisted === undefined) { | ||||
|                 result = new Promise((resolve) => { | ||||
|                     savedResolve = resolve; | ||||
|                 }); | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 provider.create(domainObject).then((response) => { | ||||
|                     this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                     savedResolve(response); | ||||
|                 }); | ||||
|             } else { | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 result = provider.update(domainObject); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     }); | ||||
| }; | ||||
|     }; | ||||
|  | ||||
| /** | ||||
|  * Search for domain objects. | ||||
|  * | ||||
|  * Object providersSearches and combines results of each object provider search. | ||||
|  * Objects without search provided will have been indexed | ||||
|  * and will be searched using the fallback indexed search. | ||||
|  * Search results are asynchronous and resolve in parallel. | ||||
|  * | ||||
|  * @method search | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  * @param {string} query the term to search for | ||||
|  * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests | ||||
|  * @returns {Array.<Promise.<module:openmct.DomainObject>>} | ||||
|  *          an array of promises returned from each object provider's search function | ||||
|  *          each resolving to domain objects matching provided search query and options. | ||||
|  */ | ||||
| ObjectAPI.prototype.search = function (query, abortSignal) { | ||||
|     const searchPromises = Object.values(this.providers) | ||||
|         .filter(provider => provider.search !== undefined) | ||||
|         .map(provider => provider.search(query, abortSignal)); | ||||
|     /** | ||||
|      * Add a root-level object. | ||||
|      * @param {module:openmct.ObjectAPI~Identifier|function} an array of | ||||
|      *        identifiers for root level objects, or a function that returns a | ||||
|      *        promise for an identifier or an array of root level objects. | ||||
|      * @method addRoot | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.addRoot = function (key) { | ||||
|         this.rootRegistry.addRoot(key); | ||||
|     }; | ||||
|  | ||||
|     searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal) | ||||
|         .then(results => results.hits | ||||
|             .map(hit => { | ||||
|                 let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId()); | ||||
|                 domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject); | ||||
|     /** | ||||
|      * Modify a domain object. | ||||
|      * @param {module:openmct.DomainObject} object the object to mutate | ||||
|      * @param {string} path the property to modify | ||||
|      * @param {*} value the new value for this property | ||||
|      * @method mutate | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.mutate = function (domainObject, path, value) { | ||||
|         const mutableObject = | ||||
|             new MutableObject(this.eventEmitter, domainObject); | ||||
|  | ||||
|                 return domainObject; | ||||
|             }))); | ||||
|         return mutableObject.set(path, value); | ||||
|     }; | ||||
|  | ||||
|     return searchPromises; | ||||
| }; | ||||
|     /** | ||||
|      * Observe changes to a domain object. | ||||
|      * @param {module:openmct.DomainObject} object the object to observe | ||||
|      * @param {string} path the property to observe | ||||
|      * @param {Function} callback a callback to invoke when new values for | ||||
|      *        this property are observed | ||||
|      * @method observe | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.observe = function (domainObject, path, callback) { | ||||
|         const mutableObject = | ||||
|             new MutableObject(this.eventEmitter, domainObject); | ||||
|         mutableObject.on(path, callback); | ||||
|  | ||||
| /** | ||||
|  * Will fetch object for the given identifier, returning a version of the object that will automatically keep | ||||
|  * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it. | ||||
|  * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The | ||||
|  * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are | ||||
|  * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed. | ||||
|  * | ||||
|  * @memberof {module:openmct.ObjectAPI#} | ||||
|  * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if | ||||
|  * the object can be mutated. | ||||
|  */ | ||||
| ObjectAPI.prototype.getMutable = function (identifier) { | ||||
|     if (!this.supportsMutation(identifier)) { | ||||
|         throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`); | ||||
|     } | ||||
|         return mutableObject.stopListening.bind(mutableObject); | ||||
|     }; | ||||
|  | ||||
|     return this.get(identifier).then((object) => { | ||||
|         return this._toMutable(object); | ||||
|     }); | ||||
| }; | ||||
|     /** | ||||
|      * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|      * @returns {string} A string representation of the given identifier, including namespace and key | ||||
|      */ | ||||
|     ObjectAPI.prototype.makeKeyString = function (identifier) { | ||||
|         return utils.makeKeyString(identifier); | ||||
|     }; | ||||
|  | ||||
| /** | ||||
|  * This function is for cleaning up a mutable domain object when you're done with it. | ||||
|  * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the | ||||
|  * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle. | ||||
|  * @param {MutableDomainObject} domainObject | ||||
|  */ | ||||
| ObjectAPI.prototype.destroyMutable = function (domainObject) { | ||||
|     if (domainObject.isMutable) { | ||||
|         return domainObject.$destroy(); | ||||
|     } else { | ||||
|         throw new Error("Attempted to destroy non-mutable domain object"); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.delete = function () { | ||||
|     throw new Error('Delete not implemented'); | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.isPersistable = function (idOrKeyString) { | ||||
|     let identifier = utils.parseKeyString(idOrKeyString); | ||||
|     let provider = this.getProvider(identifier); | ||||
|  | ||||
|     return provider !== undefined | ||||
|         && provider.create !== undefined | ||||
|         && provider.update !== undefined; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Save this domain object in its current state. EXPERIMENTAL | ||||
|  * | ||||
|  * @private | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        save | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been saved, or be rejected if it cannot be saved | ||||
|  */ | ||||
| ObjectAPI.prototype.save = function (domainObject) { | ||||
|     let provider = this.getProvider(domainObject.identifier); | ||||
|     let savedResolve; | ||||
|     let result; | ||||
|  | ||||
|     if (!this.isPersistable(domainObject.identifier)) { | ||||
|         result = Promise.reject('Object provider does not support saving'); | ||||
|     } else if (hasAlreadyBeenPersisted(domainObject)) { | ||||
|         result = Promise.resolve(true); | ||||
|     } else { | ||||
|         const persistedTime = Date.now(); | ||||
|         if (domainObject.persisted === undefined) { | ||||
|             result = new Promise((resolve) => { | ||||
|                 savedResolve = resolve; | ||||
|     /** | ||||
|      * Given any number of identifiers, will return true if they are all equal, otherwise false. | ||||
|      * @param {module:openmct.ObjectAPI~Identifier[]} identifiers | ||||
|      */ | ||||
|     ObjectAPI.prototype.areIdsEqual = function (...identifiers) { | ||||
|         return identifiers.map(utils.parseKeyString) | ||||
|             .every(identifier => { | ||||
|                 return identifier === identifiers[0] | ||||
|                     || (identifier.namespace === identifiers[0].namespace | ||||
|                         && identifier.key === identifiers[0].key); | ||||
|             }); | ||||
|             domainObject.persisted = persistedTime; | ||||
|             provider.create(domainObject).then((response) => { | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 savedResolve(response); | ||||
|             }); | ||||
|         } else { | ||||
|             domainObject.persisted = persistedTime; | ||||
|             this.mutate(domainObject, 'persisted', persistedTime); | ||||
|             result = provider.update(domainObject); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     return result; | ||||
| }; | ||||
|     ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) { | ||||
|         return this.get(identifier).then((domainObject) => { | ||||
|             path.push(domainObject); | ||||
|             let location = domainObject.location; | ||||
|  | ||||
| /** | ||||
|  * Add a root-level object. | ||||
|  * @param {module:openmct.ObjectAPI~Identifier|function} an array of | ||||
|  *        identifiers for root level objects, or a function that returns a | ||||
|  *        promise for an identifier or an array of root level objects. | ||||
|  * @method addRoot | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.addRoot = function (key) { | ||||
|     this.rootRegistry.addRoot(key); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get | ||||
|  * The domain object will be transformed after it is retrieved from the persistence store | ||||
|  * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef | ||||
|  * | ||||
|  * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add | ||||
|  * @method addGetInterceptor | ||||
|  * @memberof module:openmct.InterceptorRegistry# | ||||
|  */ | ||||
| ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) { | ||||
|     this.interceptorRegistry.addInterceptor(interceptorDef); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Retrieve the interceptors for a given domain object. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.listGetInterceptors = function (identifier, object) { | ||||
|     return this.interceptorRegistry.getInterceptors(identifier, object); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Inovke interceptors if applicable for a given domain object. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) { | ||||
|     const interceptors = this.listGetInterceptors(identifier, domainObject); | ||||
|     interceptors.forEach(interceptor => { | ||||
|         domainObject = interceptor.invoke(identifier, domainObject); | ||||
|     }); | ||||
|  | ||||
|     return domainObject; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Modify a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to mutate | ||||
|  * @param {string} path the property to modify | ||||
|  * @param {*} value the new value for this property | ||||
|  * @method mutate | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.mutate = function (domainObject, path, value) { | ||||
|     if (!this.supportsMutation(domainObject.identifier)) { | ||||
|         throw `Error: Attempted to mutate immutable object ${domainObject.name}`; | ||||
|     } | ||||
|  | ||||
|     if (domainObject.isMutable) { | ||||
|         domainObject.$set(path, value); | ||||
|     } else { | ||||
|         //Creating a temporary mutable domain object allows other mutable instances of the | ||||
|         //object to be kept in sync. | ||||
|         let mutableDomainObject = this._toMutable(domainObject); | ||||
|  | ||||
|         //Mutate original object | ||||
|         MutableDomainObject.mutateObject(domainObject, path, value); | ||||
|  | ||||
|         //Mutate temporary mutable object, in the process informing any other mutable instances | ||||
|         mutableDomainObject.$set(path, value); | ||||
|  | ||||
|         //Destroy temporary mutable object | ||||
|         this.destroyMutable(mutableDomainObject); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype._toMutable = function (object) { | ||||
|     let mutableObject; | ||||
|  | ||||
|     if (object.isMutable) { | ||||
|         mutableObject = object; | ||||
|     } else { | ||||
|         mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter); | ||||
|     } | ||||
|  | ||||
|     // Check if provider supports realtime updates | ||||
|     let identifier = utils.parseKeyString(mutableObject.identifier); | ||||
|     let provider = this.getProvider(identifier); | ||||
|  | ||||
|     if (provider !== undefined | ||||
|         && provider.observe !== undefined) { | ||||
|         let unobserve = provider.observe(identifier, (updatedModel) => { | ||||
|             mutableObject.$refresh(updatedModel); | ||||
|         }); | ||||
|         mutableObject.$on('$destroy', () => { | ||||
|             unobserve(); | ||||
|             if (location) { | ||||
|                 return this.getOriginalPath(utils.parseKeyString(location), path); | ||||
|             } else { | ||||
|                 return path; | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get | ||||
|      * The domain object will be transformed after it is retrieved from the persistence store | ||||
|      * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef | ||||
|      * | ||||
|      * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add | ||||
|      * @method addGetInterceptor | ||||
|      * @memberof module:openmct.InterceptorRegistry# | ||||
|      */ | ||||
|     ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) { | ||||
|         this.interceptorRegistry.addInterceptor(interceptorDef); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the interceptors for a given domain object. | ||||
|      * @private | ||||
|      */ | ||||
|     ObjectAPI.prototype.listGetInterceptors = function (identifier, object) { | ||||
|         return this.interceptorRegistry.getInterceptors(identifier, object); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Uniquely identifies a domain object. | ||||
|      * | ||||
|      * @typedef Identifier | ||||
|      * @memberof module:openmct.ObjectAPI~ | ||||
|      * @property {string} namespace the namespace to/from which this domain | ||||
|      *           object should be loaded/stored. | ||||
|      * @property {string} key a unique identifier for the domain object | ||||
|      *           within that namespace | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * A domain object is an entity of relevance to a user's workflow, that | ||||
|      * should appear as a distinct and meaningful object within the user | ||||
|      * interface. Examples of domain objects are folders, telemetry sensors, | ||||
|      * and so forth. | ||||
|      * | ||||
|      * A few common properties are defined for domain objects. Beyond these, | ||||
|      * individual types of domain objects may add more as they see fit. | ||||
|      * | ||||
|      * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which | ||||
|      *           uniquely identifies this domain object | ||||
|      * @property {string} type the type of domain object | ||||
|      * @property {string} name the human-readable name for this domain object | ||||
|      * @property {string} [creator] the user name of the creator of this domain | ||||
|      *           object | ||||
|      * @property {number} [modified] the time, in milliseconds since the UNIX | ||||
|      *           epoch, at which this domain object was last modified | ||||
|      * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if | ||||
|      *           present, this will be used by the default composition provider | ||||
|      *           to load domain objects | ||||
|      * @typedef DomainObject | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|  | ||||
|     function hasAlreadyBeenPersisted(domainObject) { | ||||
|         return domainObject.persisted !== undefined | ||||
|             && domainObject.persisted === domainObject.modified; | ||||
|     } | ||||
|  | ||||
|     return mutableObject; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param module:openmct.ObjectAPI~Identifier identifier An object identifier | ||||
|  * @returns {boolean} true if the object can be mutated, otherwise returns false | ||||
|  */ | ||||
| ObjectAPI.prototype.supportsMutation = function (identifier) { | ||||
|     return this.isPersistable(identifier); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Observe changes to a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to observe | ||||
|  * @param {string} path the property to observe | ||||
|  * @param {Function} callback a callback to invoke when new values for | ||||
|  *        this property are observed | ||||
|  * @method observe | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.observe = function (domainObject, path, callback) { | ||||
|     if (domainObject.isMutable) { | ||||
|         return domainObject.$observe(path, callback); | ||||
|     } else { | ||||
|         let mutable = this._toMutable(domainObject); | ||||
|         mutable.$observe(path, callback); | ||||
|  | ||||
|         return () => mutable.$destroy(); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|  * @returns {string} A string representation of the given identifier, including namespace and key | ||||
|  */ | ||||
| ObjectAPI.prototype.makeKeyString = function (identifier) { | ||||
|     return utils.makeKeyString(identifier); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon. | ||||
|  * @returns {module:openmct.ObjectAPI~Identifier} An identifier object | ||||
|  */ | ||||
| ObjectAPI.prototype.parseKeyString = function (keyString) { | ||||
|     return utils.parseKeyString(keyString); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Given any number of identifiers, will return true if they are all equal, otherwise false. | ||||
|  * @param {module:openmct.ObjectAPI~Identifier[]} identifiers | ||||
|  */ | ||||
| ObjectAPI.prototype.areIdsEqual = function (...identifiers) { | ||||
|     return identifiers.map(utils.parseKeyString) | ||||
|         .every(identifier => { | ||||
|             return identifier === identifiers[0] | ||||
|                 || (identifier.namespace === identifiers[0].namespace | ||||
|                     && identifier.key === identifiers[0].key); | ||||
|         }); | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) { | ||||
|     return this.get(identifier).then((domainObject) => { | ||||
|         path.push(domainObject); | ||||
|         let location = domainObject.location; | ||||
|  | ||||
|         if (location) { | ||||
|             return this.getOriginalPath(utils.parseKeyString(location), path); | ||||
|         } else { | ||||
|             return path; | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Uniquely identifies a domain object. | ||||
|  * | ||||
|  * @typedef Identifier | ||||
|  * @memberof module:openmct.ObjectAPI~ | ||||
|  * @property {string} namespace the namespace to/from which this domain | ||||
|  *           object should be loaded/stored. | ||||
|  * @property {string} key a unique identifier for the domain object | ||||
|  *           within that namespace | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * A domain object is an entity of relevance to a user's workflow, that | ||||
|  * should appear as a distinct and meaningful object within the user | ||||
|  * interface. Examples of domain objects are folders, telemetry sensors, | ||||
|  * and so forth. | ||||
|  * | ||||
|  * A few common properties are defined for domain objects. Beyond these, | ||||
|  * individual types of domain objects may add more as they see fit. | ||||
|  * | ||||
|  * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which | ||||
|  *           uniquely identifies this domain object | ||||
|  * @property {string} type the type of domain object | ||||
|  * @property {string} name the human-readable name for this domain object | ||||
|  * @property {string} [creator] the user name of the creator of this domain | ||||
|  *           object | ||||
|  * @property {number} [modified] the time, in milliseconds since the UNIX | ||||
|  *           epoch, at which this domain object was last modified | ||||
|  * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if | ||||
|  *           present, this will be used by the default composition provider | ||||
|  *           to load domain objects | ||||
|  * @typedef DomainObject | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
| function hasAlreadyBeenPersisted(domainObject) { | ||||
|     return domainObject.persisted !== undefined | ||||
|         && domainObject.persisted === domainObject.modified; | ||||
| } | ||||
|  | ||||
| export default ObjectAPI; | ||||
|     return ObjectAPI; | ||||
| }); | ||||
|   | ||||
| @@ -1,119 +0,0 @@ | ||||
| import ObjectAPI from './ObjectAPI.js'; | ||||
|  | ||||
| describe("The Object API Search Function", () => { | ||||
|     const MOCK_PROVIDER_KEY = 'mockProvider'; | ||||
|     const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider'; | ||||
|     const MOCK_PROVIDER_SEARCH_DELAY = 15000; | ||||
|     const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000; | ||||
|     const TOTAL_TIME_ELAPSED = 21000; | ||||
|     const BASE_TIME = new Date(2021, 0, 1); | ||||
|  | ||||
|     let objectAPI; | ||||
|     let mockObjectProvider; | ||||
|     let anotherMockObjectProvider; | ||||
|     let mockFallbackProvider; | ||||
|     let fallbackProviderSearchResults; | ||||
|     let resultsPromises; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         jasmine.clock().install(); | ||||
|         jasmine.clock().mockDate(BASE_TIME); | ||||
|  | ||||
|         resultsPromises = []; | ||||
|         fallbackProviderSearchResults = { | ||||
|             hits: [] | ||||
|         }; | ||||
|  | ||||
|         objectAPI = new ObjectAPI(); | ||||
|  | ||||
|         mockObjectProvider = jasmine.createSpyObj("mock object provider", [ | ||||
|             "search" | ||||
|         ]); | ||||
|         anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ | ||||
|             "search" | ||||
|         ]); | ||||
|         mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [ | ||||
|             "superSecretFallbackSearch" | ||||
|         ]); | ||||
|         objectAPI.addProvider('objects', mockObjectProvider); | ||||
|         objectAPI.addProvider('other-objects', anotherMockObjectProvider); | ||||
|         objectAPI.supersecretSetFallbackProvider(mockFallbackProvider); | ||||
|  | ||||
|         mockObjectProvider.search.and.callFake(() => { | ||||
|             return new Promise(resolve => { | ||||
|                 const mockProviderSearch = { | ||||
|                     name: MOCK_PROVIDER_KEY, | ||||
|                     start: new Date() | ||||
|                 }; | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     mockProviderSearch.end = new Date(); | ||||
|  | ||||
|                     return resolve(mockProviderSearch); | ||||
|                 }, MOCK_PROVIDER_SEARCH_DELAY); | ||||
|             }); | ||||
|         }); | ||||
|         anotherMockObjectProvider.search.and.callFake(() => { | ||||
|             return new Promise(resolve => { | ||||
|                 const anotherMockProviderSearch = { | ||||
|                     name: ANOTHER_MOCK_PROVIDER_KEY, | ||||
|                     start: new Date() | ||||
|                 }; | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     anotherMockProviderSearch.end = new Date(); | ||||
|  | ||||
|                     return resolve(anotherMockProviderSearch); | ||||
|                 }, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY); | ||||
|             }); | ||||
|         }); | ||||
|         mockFallbackProvider.superSecretFallbackSearch.and.callFake( | ||||
|             () => new Promise( | ||||
|                 resolve => setTimeout( | ||||
|                     () => resolve(fallbackProviderSearchResults), | ||||
|                     50 | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|         resultsPromises = objectAPI.search('foo'); | ||||
|  | ||||
|         jasmine.clock().tick(TOTAL_TIME_ELAPSED); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         jasmine.clock().uninstall(); | ||||
|     }); | ||||
|  | ||||
|     it("uses each objects given provider's search function", () => { | ||||
|         expect(mockObjectProvider.search).toHaveBeenCalled(); | ||||
|         expect(anotherMockObjectProvider.search).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("uses the fallback indexed search for objects without a search function provided", () => { | ||||
|         expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("provides each providers results as promises that resolve in parallel", async () => { | ||||
|         const results = await Promise.all(resultsPromises); | ||||
|         const mockProviderResults = results.find( | ||||
|             result => result.name === MOCK_PROVIDER_KEY | ||||
|         ); | ||||
|         const anotherMockProviderResults = results.find( | ||||
|             result => result.name === ANOTHER_MOCK_PROVIDER_KEY | ||||
|         ); | ||||
|         const mockProviderStart = mockProviderResults.start.getTime(); | ||||
|         const mockProviderEnd = mockProviderResults.end.getTime(); | ||||
|         const anotherMockProviderStart = anotherMockProviderResults.start.getTime(); | ||||
|         const anotherMockProviderEnd = anotherMockProviderResults.end.getTime(); | ||||
|         const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd) | ||||
|             - Math.min(mockProviderEnd, anotherMockProviderEnd); | ||||
|  | ||||
|         expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd); | ||||
|         expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd); | ||||
|         expect(searchElapsedTime).toBeLessThan( | ||||
|             MOCK_PROVIDER_SEARCH_DELAY | ||||
|             + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY | ||||
|         ); | ||||
|     }); | ||||
| }); | ||||
| @@ -2,30 +2,12 @@ import ObjectAPI from './ObjectAPI.js'; | ||||
|  | ||||
| describe("The Object API", () => { | ||||
|     let objectAPI; | ||||
|     let typeRegistry; | ||||
|     let openmct = {}; | ||||
|     let mockIdentifierService; | ||||
|     let mockDomainObject; | ||||
|     const TEST_NAMESPACE = "test-namespace"; | ||||
|     const FIFTEEN_MINUTES = 15 * 60 * 1000; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         typeRegistry = jasmine.createSpyObj('typeRegistry', [ | ||||
|             'get' | ||||
|         ]); | ||||
|         openmct.$injector = jasmine.createSpyObj('$injector', ['get']); | ||||
|         mockIdentifierService = jasmine.createSpyObj( | ||||
|             'identifierService', | ||||
|             ['parse'] | ||||
|         ); | ||||
|         mockIdentifierService.parse.and.returnValue({ | ||||
|             getSpace: () => { | ||||
|                 return TEST_NAMESPACE; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         openmct.$injector.get.and.returnValue(mockIdentifierService); | ||||
|         objectAPI = new ObjectAPI(typeRegistry, openmct); | ||||
|         objectAPI = new ObjectAPI(); | ||||
|         mockDomainObject = { | ||||
|             identifier: { | ||||
|                 namespace: TEST_NAMESPACE, | ||||
| @@ -51,7 +33,6 @@ describe("The Object API", () => { | ||||
|                     "update" | ||||
|                 ]); | ||||
|                 mockProvider.create.and.returnValue(Promise.resolve(true)); | ||||
|                 mockProvider.update.and.returnValue(Promise.resolve(true)); | ||||
|                 objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|             }); | ||||
|             it("Calls 'create' on provider if object is new", () => { | ||||
| @@ -147,155 +128,4 @@ describe("The Object API", () => { | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the mutation API", () => { | ||||
|         let testObject; | ||||
|         let updatedTestObject; | ||||
|         let mutable; | ||||
|         let mockProvider; | ||||
|         let callbacks = []; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             objectAPI = new ObjectAPI(typeRegistry, openmct); | ||||
|             testObject = { | ||||
|                 identifier: { | ||||
|                     namespace: TEST_NAMESPACE, | ||||
|                     key: 'test-key' | ||||
|                 }, | ||||
|                 name: 'test object', | ||||
|                 otherAttribute: 'other-attribute-value', | ||||
|                 objectAttribute: { | ||||
|                     embeddedObject: { | ||||
|                         embeddedKey: 'embedded-value' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject); | ||||
|             mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                 "get", | ||||
|                 "create", | ||||
|                 "update", | ||||
|                 "observe", | ||||
|                 "observeObjectChanges" | ||||
|             ]); | ||||
|             mockProvider.get.and.returnValue(Promise.resolve(testObject)); | ||||
|             mockProvider.observeObjectChanges.and.callFake(() => { | ||||
|                 callbacks[0](updatedTestObject); | ||||
|                 callbacks.splice(0, 1); | ||||
|             }); | ||||
|             mockProvider.observe.and.callFake((id, callback) => { | ||||
|                 if (callbacks.length === 0) { | ||||
|                     callbacks.push(callback); | ||||
|                 } else { | ||||
|                     callbacks[0] = callback; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|  | ||||
|             return objectAPI.getMutable(testObject.identifier) | ||||
|                 .then(object => { | ||||
|                     mutable = object; | ||||
|  | ||||
|                     return mutable; | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             mutable.$destroy(); | ||||
|         }); | ||||
|  | ||||
|         it('mutates the original object', () => { | ||||
|             const MUTATED_NAME = 'mutated name'; | ||||
|             objectAPI.mutate(testObject, 'name', MUTATED_NAME); | ||||
|             expect(testObject.name).toBe(MUTATED_NAME); | ||||
|         }); | ||||
|  | ||||
|         describe ('uses a MutableDomainObject', () => { | ||||
|             it('and retains properties of original object ', function () { | ||||
|                 expect(hasOwnProperty(mutable, 'identifier')).toBe(true); | ||||
|                 expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true); | ||||
|                 expect(mutable.identifier).toEqual(testObject.identifier); | ||||
|                 expect(mutable.otherAttribute).toEqual(testObject.otherAttribute); | ||||
|             }); | ||||
|  | ||||
|             it('that is identical to original object when serialized', function () { | ||||
|                 expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject)); | ||||
|             }); | ||||
|  | ||||
|             it('that observes for object changes', function () { | ||||
|                 let mockListener = jasmine.createSpy('mockListener'); | ||||
|                 objectAPI.observe(testObject, '*', mockListener); | ||||
|                 mockProvider.observeObjectChanges(); | ||||
|                 expect(mockListener).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('uses events', function () { | ||||
|             let testObjectDuplicate; | ||||
|             let mutableSecondInstance; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 // Duplicate object to guarantee we are not sharing object instance, which would invalidate test | ||||
|                 testObjectDuplicate = JSON.parse(JSON.stringify(testObject)); | ||||
|                 mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate); | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 mutableSecondInstance.$destroy(); | ||||
|             }); | ||||
|  | ||||
|             it('to stay synchronized when mutated', function () { | ||||
|                 objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); | ||||
|                 expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a property changes', function () { | ||||
|                 let mutationCallback = jasmine.createSpy('mutation-callback'); | ||||
|                 let unlisten; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     mutationCallback.and.callFake(resolve); | ||||
|                     unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); | ||||
|                     objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); | ||||
|                     unlisten(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a child property has changed', function () { | ||||
|                 let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                 let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                 let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                 let listeners = []; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                     objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                     expect(embeddedObjectCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedKey: 'updated-embedded-value' | ||||
|                     }); | ||||
|                     expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedObject: { | ||||
|                             embeddedKey: 'updated-embedded-value' | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     listeners.forEach(listener => listener()); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| function hasOwnProperty(object, property) { | ||||
|     return Object.prototype.hasOwnProperty.call(object, property); | ||||
| } | ||||
|   | ||||
| @@ -48,7 +48,7 @@ define([ | ||||
|             this.providers.push(function () { | ||||
|                 return key; | ||||
|             }); | ||||
|         } else if (typeof key === "function") { | ||||
|         } else if (_.isFunction(key)) { | ||||
|             this.providers.push(key); | ||||
|         } | ||||
|     }; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
| @@ -20,4 +20,13 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| export const ALLOWED_FOLDER_TYPES = ['folder', 'noneditable.folder']; | ||||
| define([ | ||||
|     "EventEmitter" | ||||
| ], function ( | ||||
|     EventEmitter | ||||
| ) { | ||||
|     /** | ||||
|      * Provides a singleton event bus for sharing between objects. | ||||
|      */ | ||||
|     return new EventEmitter(); | ||||
| }); | ||||
| @@ -6,9 +6,6 @@ class Dialog extends Overlay { | ||||
|     constructor({iconClass, message, title, hint, timestamp, ...options}) { | ||||
|  | ||||
|         let component = new Vue({ | ||||
|             components: { | ||||
|                 DialogComponent: DialogComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 iconClass, | ||||
|                 message, | ||||
| @@ -16,6 +13,9 @@ class Dialog extends Overlay { | ||||
|                 hint, | ||||
|                 timestamp | ||||
|             }, | ||||
|             components: { | ||||
|                 DialogComponent: DialogComponent | ||||
|             }, | ||||
|             template: '<dialog-component></dialog-component>' | ||||
|         }).$mount(); | ||||
|  | ||||
|   | ||||
| @@ -7,9 +7,6 @@ let component; | ||||
| class ProgressDialog extends Overlay { | ||||
|     constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) { | ||||
|         component = new Vue({ | ||||
|             components: { | ||||
|                 ProgressDialogComponent: ProgressDialogComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 iconClass, | ||||
|                 message, | ||||
| @@ -17,6 +14,9 @@ class ProgressDialog extends Overlay { | ||||
|                 hint, | ||||
|                 timestamp | ||||
|             }, | ||||
|             components: { | ||||
|                 ProgressDialogComponent: ProgressDialogComponent | ||||
|             }, | ||||
|             data() { | ||||
|                 return { | ||||
|                     model: { | ||||
|   | ||||
| @@ -38,12 +38,12 @@ | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['dismiss', 'element', 'buttons', 'dismissable'], | ||||
|     data: function () { | ||||
|         return { | ||||
|             focusIndex: -1 | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['dismiss', 'element', 'buttons', 'dismissable'], | ||||
|     mounted() { | ||||
|         const element = this.$refs.element; | ||||
|         element.appendChild(this.element); | ||||
|   | ||||
| @@ -20,6 +20,8 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
|  | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
| @@ -273,6 +275,48 @@ define([ | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request telemetry collection for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
|      * (start, end, etc.), sort order, and strategies for retrieving | ||||
|      * telemetry (aggregation, latest available, etc.). | ||||
|      * | ||||
|      * @method request | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the object | ||||
|      *        which has associated telemetry | ||||
|      * @param {module:openmct.TelemetryAPI~TelemetryRequest} options | ||||
|      *        options for this historical request | ||||
|      * @returns {Promise.<object[]>} a promise for an array of | ||||
|      *          telemetry data | ||||
|      */ | ||||
|     TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject) { | ||||
|         if (arguments.length === 1) { | ||||
|             arguments.length = 2; | ||||
|             arguments[1] = {}; | ||||
|         } | ||||
|  | ||||
|         // historical setup | ||||
|         this.standardizeRequestOptions(arguments[1]); | ||||
|         const historicalProvider = this.findRequestProvider(domainObject, arguments); | ||||
|  | ||||
|         // subscription setup | ||||
|         const subscriptionProvider = this.findSubscriptionProvider(domainObject); | ||||
|  | ||||
|         // check for no providers | ||||
|         if (!historicalProvider && !subscriptionProvider) { | ||||
|             return Promise.reject('No providers found'); | ||||
|         } | ||||
|  | ||||
|         let telemetryCollectionOptions = { | ||||
|             historicalProvider, | ||||
|             subscriptionProvider, | ||||
|             arguments: arguments | ||||
|         }; | ||||
|  | ||||
|         return Promise.resolve(new TelemetryCollection(this.openmct, domainObject, telemetryCollectionOptions)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request historical telemetry for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
|   | ||||
							
								
								
									
										315
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,315 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| // import _ from 'lodash'; | ||||
| import TelemetrySubscriptionService from './TelemetrySubscriptionService'; | ||||
|  | ||||
| function bindUs() { | ||||
|     return [ | ||||
|         'trackHistoricalTelemetry', | ||||
|         'trackSubscriptionTelemetry', | ||||
|         'addPage', | ||||
|         'processNewTelemetry', | ||||
|         'hasMorePages', | ||||
|         'nextPage', | ||||
|         'bounds', | ||||
|         'timeSystem', | ||||
|         'on', | ||||
|         'off', | ||||
|         'emit', | ||||
|         'subscribeToBounds', | ||||
|         'unsubscribeFromBounds', | ||||
|         'subscribeToTimeSystem', | ||||
|         'unsubscribeFromTimeSystem', | ||||
|         'destroy' | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| export class TelemetryCollection { | ||||
|  | ||||
|     constructor(openmct, domainObject, options) { | ||||
|         bindUs().forEach(method => this[method] = this[method].bind(this)); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|         this.parseTime = undefined; | ||||
|         this.timeSystem(openmct.time.timeSystem()); | ||||
|         this.lastBounds = openmct.time.bounds(); | ||||
|  | ||||
|         this.historicalProvider = options.historicalProvider; | ||||
|         this.subscriptionProvider = options.subscriptionProvider; | ||||
|  | ||||
|         this.arguments = options.arguments; | ||||
|  | ||||
|         this.listeners = { | ||||
|             add: [], | ||||
|             remove: [] | ||||
|         }; | ||||
|  | ||||
|         this.trackHistoricalTelemetry(); | ||||
|         this.trackSubscriptionTelemetry(); | ||||
|  | ||||
|         this.subscribeToBounds(); | ||||
|         this.subscribeToTimeSystem(); | ||||
|     } | ||||
|  | ||||
|     // should we wait to track history until an 'add' listener is added? | ||||
|     async trackHistoricalTelemetry() { | ||||
|         if (!this.historicalProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // remove for reset | ||||
|         if (this.boundedTelemetry.length !== 0) { | ||||
|             this.emit('remove', this.boundedTelemetry); | ||||
|             this.boundedTelemetry = []; | ||||
|         } | ||||
|  | ||||
|         let historicalData = await this.historicalProvider.request.apply(this.domainObject, this.arguments).catch((rejected) => { | ||||
|             this.openmct.notifications.error('Error requesting telemetry data, see console for details'); | ||||
|             console.error(rejected); | ||||
|  | ||||
|             return Promise.reject(rejected); | ||||
|         }); | ||||
|  | ||||
|         // make sure it wasn't rejected | ||||
|         if (Array.isArray(historicalData)) { | ||||
|             // reset on requests, should only happen on initial load, | ||||
|             // bounds manually changed and time system changes | ||||
|             this.boundedTelemetry = historicalData; | ||||
|             this.emit('add', [...this.boundedTelemetry]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     trackSubscriptionTelemetry() { | ||||
|         if (!this.subscriptionProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.subscriptionService = new TelemetrySubscriptionService(this.openmct); | ||||
|         this.unsubscribe = this.subscriptionService.subscribe( | ||||
|             this.domainObject, | ||||
|             this.processNewTelemetry, | ||||
|             this.subscriptionProvider, | ||||
|             this.arguments | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // utilized by telemetry provider to add more data | ||||
|     addPage(telemetryData) { | ||||
|         this.processNewTelemetry(telemetryData); | ||||
|     } | ||||
|  | ||||
|     // used to sort any new telemetry (add/page, historical, subscription) | ||||
|     processNewTelemetry(telemetryData) { | ||||
|  | ||||
|         let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; | ||||
|         let parsedValue; | ||||
|         let beforeStartOfBounds; | ||||
|         let afterEndOfBounds; | ||||
|         let added = []; | ||||
|  | ||||
|         for (let datum of data) { | ||||
|             parsedValue = this.parseTime(datum); | ||||
|             beforeStartOfBounds = parsedValue < this.lastBounds.start; | ||||
|             afterEndOfBounds = parsedValue > this.lastBounds.end; | ||||
|  | ||||
|             if (!afterEndOfBounds && !beforeStartOfBounds) { | ||||
|                 if (!this.boundedTelemetry.includes(datum)) { | ||||
|                     this.boundedTelemetry.push(datum); | ||||
|                     added.push(datum); | ||||
|                 } | ||||
|             } else if (afterEndOfBounds) { | ||||
|                 this.futureBuffer.push(datum); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (added.length) { | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // returns a boolean if there is more telemetry within the time bounds  | ||||
|     // if the provider supports it | ||||
|     hasMorePages() { | ||||
|         return this.historicalProvider | ||||
|             && this.historicalProvider.supportsPaging() | ||||
|             && this.historicalProvider.hasMorePages(this); | ||||
|     } | ||||
|  | ||||
|     // will return the next "page" of telemetry if the provider supports it | ||||
|     nextPage() { | ||||
|         if (!this.historicalProvider || !this.historicalProvider.supportsPaging()) { | ||||
|             throw new Error('Provider does not support paging'); | ||||
|         } | ||||
|  | ||||
|         this.historicalProvider.nextPage(this.arguments, this); | ||||
|     } | ||||
|  | ||||
|     // when user changes bounds, or when bounds increment from a tick | ||||
|     bounds(bounds, isTick) { | ||||
|  | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (isTick) { | ||||
|             // need to check futureBuffer and need to check | ||||
|             // if anything has fallen out of bounds | ||||
|         } else { | ||||
|             // TODO: also reset right? | ||||
|             // need to reset and request history again | ||||
|             // no need to mess with subscription | ||||
|         } | ||||
|  | ||||
|         let startChanged = this.lastBounds.start !== bounds.start; | ||||
|         let endChanged = this.lastBounds.end !== bounds.end; | ||||
|  | ||||
|         let startIndex = 0; | ||||
|         let endIndex = 0; | ||||
|  | ||||
|         let discarded = []; | ||||
|         let added = []; | ||||
|         let testValue = { | ||||
|             datum: {} | ||||
|         }; | ||||
|  | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (startChanged) { | ||||
|             testValue.datum[this.sortOptions.key] = bounds.start; | ||||
|             // Calculate the new index of the first item within the bounds | ||||
|             startIndex = this.sortedIndex(this.rows, testValue); | ||||
|             discarded = this.rows.splice(0, startIndex); | ||||
|         } | ||||
|  | ||||
|         if (endChanged) { | ||||
|             testValue.datum[this.sortOptions.key] = bounds.end; | ||||
|             // Calculate the new index of the last item in bounds | ||||
|             endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue); | ||||
|             added = this.futureBuffer.rows.splice(0, endIndex); | ||||
|             added.forEach((datum) => this.rows.push(datum)); | ||||
|         } | ||||
|  | ||||
|         if (discarded.length > 0) { | ||||
|             /** | ||||
|              * A `discarded` event is emitted when telemetry data fall out of | ||||
|              * bounds due to a bounds change event | ||||
|              * @type {object[]} discarded the telemetry data | ||||
|              * discarded as a result of the bounds change | ||||
|              */ | ||||
|             this.emit('remove', discarded); | ||||
|         } | ||||
|  | ||||
|         if (added.length > 0) { | ||||
|             /** | ||||
|              * An `added` event is emitted when a bounds change results in | ||||
|              * received telemetry falling within the new bounds. | ||||
|              * @type {object[]} added the telemetry data that is now within bounds | ||||
|              */ | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     timeSystem(timeSystem) { | ||||
|         let timeKey = timeSystem.key; | ||||
|         let formatter = this.openmct.telemetry.getValueFormatter({ | ||||
|             key: timeKey, | ||||
|             source: timeKey, | ||||
|             format: timeKey | ||||
|         }); | ||||
|  | ||||
|         this.parseTime = formatter.parse; | ||||
|  | ||||
|         // TODO: Reset right? | ||||
|     } | ||||
|  | ||||
|     on(event, callback, context) { | ||||
|         if (!this.listeners[event]) { | ||||
|             throw new Error('Event not supported by Telemetry Collections: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (this.listeners[event].includes(callback)) { | ||||
|             throw new Error('Tried to add a listener that is already registered'); | ||||
|         } | ||||
|  | ||||
|         this.listeners[event].push({ | ||||
|             callback: callback, | ||||
|             context: context | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Unregister TelemetryCollection events. | ||||
|     off(event, callback) { | ||||
|         if (!this.listeners[event]) { | ||||
|             throw new Error('Event not supported by Telemetry Collections: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (!this.listeners[event].includes(callback)) { | ||||
|             throw new Error('Tried to remove a listener that does not exist'); | ||||
|         } | ||||
|  | ||||
|         this.listeners[event].remove(callback); | ||||
|     } | ||||
|  | ||||
|     emit(event, payload) { | ||||
|         if (!this.listeners[event].length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         payload = [...payload]; | ||||
|  | ||||
|         this.listeners[event].forEach((listener) => { | ||||
|             if (listener.context) { | ||||
|                 listener.callback.apply(listener.context, payload); | ||||
|             } else { | ||||
|                 listener.callback(payload); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     subscribeToBounds() { | ||||
|         this.openmct.time.on('bounds', this.bounds); | ||||
|     } | ||||
|  | ||||
|     unsubscribeFromBounds() { | ||||
|         this.openmct.time.off('bounds', this.bounds); | ||||
|     } | ||||
|  | ||||
|     subscribeToTimeSystem() { | ||||
|         this.openmct.time.on('timeSystem', this.timeSystem); | ||||
|     } | ||||
|  | ||||
|     unsubscribeFromTimeSystem() { | ||||
|         this.openmct.time.off('bounds', this.timeSystem); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.unsubscribeFromBounds(); | ||||
|         this.unsubscribeFromTimeSystem(); | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/api/telemetry/TelemetrySubscriptionService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/api/telemetry/TelemetrySubscriptionService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import objectUtils from 'objectUtils'; | ||||
| class TelemetrySubscriptionService { | ||||
|     constructor(openmct) { | ||||
|         if (!TelemetrySubscriptionService.instance) { | ||||
|             this.openmct = openmct; | ||||
|             this.subscriptionCache = {}; | ||||
|  | ||||
|             TelemetrySubscriptionService.instance = this; | ||||
|         } | ||||
|  | ||||
|         return TelemetrySubscriptionService.instance; // eslint-disable-line no-constructor-return | ||||
|     } | ||||
|  | ||||
|     subscribe(domainObject, callback, provider, options) { | ||||
|         const keyString = objectUtils.makeKeyString(domainObject.identifier); | ||||
|         let subscriber = this.subscriptionCache[keyString]; | ||||
|  | ||||
|         if (!subscriber) { | ||||
|             subscriber = this.subscriptionCache[keyString] = { | ||||
|                 callbacks: [callback] | ||||
|             }; | ||||
|  | ||||
|             subscriber.unsubscribe = provider | ||||
|                 .subscribe(domainObject, function (value) { | ||||
|                     subscriber.callbacks.forEach(function (cb) { | ||||
|                         cb(value); | ||||
|                     }); | ||||
|                 }, options); | ||||
|         } else { | ||||
|             subscriber.callbacks.push(callback); | ||||
|         } | ||||
|  | ||||
|         return () => { | ||||
|             subscriber.callbacks = subscriber.callbacks.filter((cb) => { | ||||
|                 return cb !== callback; | ||||
|             }); | ||||
|             if (subscriber.callbacks.length === 0) { | ||||
|                 subscriber.unsubscribe(); | ||||
|                 delete this.subscriptionCache[keyString]; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| function instance(openmct) { | ||||
|     return new TelemetrySubscriptionService(openmct); | ||||
| } | ||||
|  | ||||
| export default instance; | ||||
| @@ -1,37 +0,0 @@ | ||||
| export default function (folderName, couchPlugin, searchFilter) { | ||||
|     return function install(openmct) { | ||||
|         const couchProvider = couchPlugin.couchProvider; | ||||
|  | ||||
|         openmct.objects.addRoot({ | ||||
|             namespace: 'couch-search', | ||||
|             key: 'couch-search' | ||||
|         }); | ||||
|  | ||||
|         openmct.objects.addProvider('couch-search', { | ||||
|             get(identifier) { | ||||
|                 if (identifier.key !== 'couch-search') { | ||||
|                     return undefined; | ||||
|                 } else { | ||||
|                     return Promise.resolve({ | ||||
|                         identifier, | ||||
|                         type: 'folder', | ||||
|                         name: folderName || "CouchDB Documents" | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         openmct.composition.addProvider({ | ||||
|             appliesTo(domainObject) { | ||||
|                 return domainObject.identifier.namespace === 'couch-search' | ||||
|                     && domainObject.identifier.key === 'couch-search'; | ||||
|             }, | ||||
|             load() { | ||||
|                 return couchProvider.getObjectsByFilter(searchFilter).then(objects => { | ||||
|                     return objects.map(object => object.identifier); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
| } | ||||
| @@ -1,91 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import {createOpenMct, resetApplicationState} from "utils/testing"; | ||||
| import CouchDBSearchFolderPlugin from './plugin'; | ||||
|  | ||||
| describe('the plugin', function () { | ||||
|     let identifier = { | ||||
|         namespace: 'couch-search', | ||||
|         key: "couch-search" | ||||
|     }; | ||||
|     let testPath = '/test/db'; | ||||
|     let openmct; | ||||
|     let composition; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         let couchPlugin = openmct.plugins.CouchDB(testPath); | ||||
|         openmct.install(couchPlugin); | ||||
|  | ||||
|         openmct.install(new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, { | ||||
|             "selector": { | ||||
|                 "model": { | ||||
|                     "type": "plan" | ||||
|                 } | ||||
|             } | ||||
|         })); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         composition = openmct.composition.get({identifier}); | ||||
|  | ||||
|         spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ | ||||
|             { | ||||
|                 identifier: { | ||||
|                     key: "1", | ||||
|                     namespace: "mct" | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 identifier: { | ||||
|                     key: "2", | ||||
|                     namespace: "mct" | ||||
|                 } | ||||
|             } | ||||
|         ])); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('provides a folder to hold plans', () => { | ||||
|         openmct.objects.get(identifier).then((object) => { | ||||
|             expect(object).toEqual({ | ||||
|                 identifier, | ||||
|                 type: 'folder', | ||||
|                 name: "CouchDB Documents" | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('provides composition for couch search folders', () => { | ||||
|         composition.load().then((objects) => { | ||||
|             expect(objects.length).toEqual(2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -44,15 +44,11 @@ export default function LADTableViewProvider(openmct) { | ||||
|                             LadTableComponent: LadTable | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct | ||||
|                             openmct, | ||||
|                             domainObject, | ||||
|                             objectPath | ||||
|                         }, | ||||
|                         data: () => { | ||||
|                             return { | ||||
|                                 domainObject, | ||||
|                                 objectPath | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>' | ||||
|                         template: '<lad-table-component></lad-table-component>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|     class="js-lad-table__body__row" | ||||
|     @contextmenu.prevent="showContextMenu" | ||||
| > | ||||
|     <td class="js-first-data">{{ domainObject.name }}</td> | ||||
|     <td class="js-first-data">{{ name }}</td> | ||||
|     <td class="js-second-data">{{ formattedTimestamp }}</td> | ||||
|     <td | ||||
|         class="js-third-data" | ||||
| @@ -50,16 +50,12 @@ const CONTEXT_MENU_ACTIONS = [ | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         objectPath: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         }, | ||||
|         hasUnits: { | ||||
|             type: Boolean, | ||||
|             requred: true | ||||
| @@ -70,6 +66,7 @@ export default { | ||||
|         currentObjectPath.unshift(this.domainObject); | ||||
|  | ||||
|         return { | ||||
|             name: this.domainObject.name, | ||||
|             timestamp: undefined, | ||||
|             value: '---', | ||||
|             valueClass: '', | ||||
| @@ -92,6 +89,14 @@ export default { | ||||
|             .telemetry | ||||
|             .limitEvaluator(this.domainObject); | ||||
|  | ||||
|         this.stopWatchingMutation = this.openmct | ||||
|             .objects | ||||
|             .observe( | ||||
|                 this.domainObject, | ||||
|                 '*', | ||||
|                 this.updateName | ||||
|             ); | ||||
|  | ||||
|         this.openmct.time.on('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.on('bounds', this.updateBounds); | ||||
|  | ||||
| @@ -114,6 +119,7 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.stopWatchingMutation(); | ||||
|         this.unsubscribe(); | ||||
|         this.openmct.time.off('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.off('bounds', this.updateBounds); | ||||
| @@ -154,6 +160,9 @@ export default { | ||||
|                 }) | ||||
|                 .then((array) => this.updateValues(array[array.length - 1])); | ||||
|         }, | ||||
|         updateName(name) { | ||||
|             this.name = name; | ||||
|         }, | ||||
|         updateBounds(bounds, isTick) { | ||||
|             this.bounds = bounds; | ||||
|             if (!isTick) { | ||||
|   | ||||
| @@ -36,7 +36,6 @@ | ||||
|                 v-for="item in items" | ||||
|                 :key="item.key" | ||||
|                 :domain-object="item.domainObject" | ||||
|                 :object-path="objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|             /> | ||||
|         </tbody> | ||||
| @@ -48,20 +47,10 @@ | ||||
| import LadRow from './LADRow.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'objectPath'], | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         objectPath: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             items: [] | ||||
|   | ||||
| @@ -57,10 +57,10 @@ | ||||
| import LadRow from './LADRow.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             ladTableObjects: [], | ||||
|   | ||||
| @@ -98,7 +98,7 @@ describe("The LAD Table", () => { | ||||
|     }); | ||||
|  | ||||
|     it("should provide a table view only for lad table objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(mockObj.ladTable, []); | ||||
|         let applicableViews = openmct.objectViews.get(mockObj.ladTable); | ||||
|  | ||||
|         let ladTableView = applicableViews.find( | ||||
|             (viewProvider) => viewProvider.key === ladTableKey | ||||
| @@ -185,7 +185,7 @@ describe("The LAD Table", () => { | ||||
|                 end: bounds.end | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(mockObj.ladTable, []); | ||||
|             applicableViews = openmct.objectViews.get(mockObj.ladTable); | ||||
|             ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey); | ||||
|             ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]); | ||||
|             ladTableView.show(child, true); | ||||
| @@ -296,7 +296,7 @@ describe("The LAD Table Set", () => { | ||||
|     }); | ||||
|  | ||||
|     it("should provide a lad table set view only for lad table set objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); | ||||
|         let applicableViews = openmct.objectViews.get(mockObj.ladTableSet); | ||||
|  | ||||
|         let ladTableSetView = applicableViews.find( | ||||
|             (viewProvider) => viewProvider.key === ladTableSetKey | ||||
| @@ -391,7 +391,7 @@ describe("The LAD Table Set", () => { | ||||
|                 end: bounds.end | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); | ||||
|             applicableViews = openmct.objectViews.get(mockObj.ladTableSet); | ||||
|             ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey); | ||||
|             ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); | ||||
|             ladTableSetView.show(child, true); | ||||
|   | ||||
| @@ -67,11 +67,11 @@ describe("AutoflowTabularPlugin", () => { | ||||
|         }); | ||||
|  | ||||
|         it("applies its view to the type from options", () => { | ||||
|             expect(provider.canView(testObject, [])).toBe(true); | ||||
|             expect(provider.canView(testObject)).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("does not apply to other types", () => { | ||||
|             expect(provider.canView({ type: 'foo' }, [])).toBe(false); | ||||
|             expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         describe("provides a view which", () => { | ||||
|   | ||||
| @@ -37,12 +37,12 @@ define([ | ||||
|         return function install(openmct) { | ||||
|             if (installIndicator) { | ||||
|                 let component = new Vue ({ | ||||
|                     components: { | ||||
|                         GlobalClearIndicator: GlobaClearIndicator.default | ||||
|                     }, | ||||
|                     provide: { | ||||
|                         openmct | ||||
|                     }, | ||||
|                     components: { | ||||
|                         GlobalClearIndicator: GlobaClearIndicator.default | ||||
|                     }, | ||||
|                     template: '<GlobalClearIndicator></GlobalClearIndicator>' | ||||
|                 }); | ||||
|  | ||||
|   | ||||
| @@ -75,8 +75,7 @@ export default class Condition extends EventEmitter { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate | ||||
|         if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) { | ||||
|         if (this.isTelemetryUsed(datum.id)) { | ||||
|  | ||||
|             this.criteria.forEach(criterion => { | ||||
|                 if (this.isAnyOrAllTelemetry(criterion)) { | ||||
| @@ -94,12 +93,6 @@ export default class Condition extends EventEmitter { | ||||
|         return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')); | ||||
|     } | ||||
|  | ||||
|     hasNoTelemetry() { | ||||
|         return this.criteria.every((criterion) => { | ||||
|             return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === ''; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     isTelemetryUsed(id) { | ||||
|         return this.criteria.some(criterion => { | ||||
|             return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id; | ||||
| @@ -257,17 +250,10 @@ export default class Condition extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     getTriggerDescription() { | ||||
|         if (this.trigger) { | ||||
|             return { | ||||
|                 conjunction: TRIGGER_CONJUNCTION[this.trigger], | ||||
|                 prefix: `${TRIGGER_LABEL[this.trigger]}: ` | ||||
|             }; | ||||
|         } else { | ||||
|             return { | ||||
|                 conjunction: '', | ||||
|                 prefix: '' | ||||
|             }; | ||||
|         } | ||||
|         return { | ||||
|             conjunction: TRIGGER_CONJUNCTION[this.trigger], | ||||
|             prefix: `${TRIGGER_LABEL[this.trigger]}: ` | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     requestLADConditionResult() { | ||||
|   | ||||
| @@ -34,9 +34,6 @@ export default class ConditionManager extends EventEmitter { | ||||
|         this.composition = this.openmct.composition.get(conditionSetDomainObject); | ||||
|         this.composition.on('add', this.subscribeToTelemetry, this); | ||||
|         this.composition.on('remove', this.unsubscribeFromTelemetry, this); | ||||
|  | ||||
|         this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); | ||||
|  | ||||
|         this.compositionLoad = this.composition.load(); | ||||
|         this.subscriptions = {}; | ||||
|         this.telemetryObjects = {}; | ||||
| @@ -82,17 +79,6 @@ export default class ConditionManager extends EventEmitter { | ||||
|         delete this.subscriptions[id]; | ||||
|         delete this.telemetryObjects[id]; | ||||
|         this.removeConditionTelemetryObjects(); | ||||
|  | ||||
|         //force re-computation of condition set result as we might be in a state where | ||||
|         // there is no telemetry datum coming in for a while or at all. | ||||
|         let latestTimestamp = getLatestTimestamp( | ||||
|             {}, | ||||
|             {}, | ||||
|             this.timeSystems, | ||||
|             this.openmct.time.timeSystem() | ||||
|         ); | ||||
|         this.updateConditionResults({id: id}); | ||||
|         this.updateCurrentCondition(latestTimestamp); | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
| @@ -340,10 +326,6 @@ export default class ConditionManager extends EventEmitter { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     shouldEvaluateNewTelemetry(currentTimestamp) { | ||||
|         return this.openmct.time.bounds().end >= currentTimestamp; | ||||
|     } | ||||
|  | ||||
|     telemetryReceived(endpoint, datum) { | ||||
|         if (!this.isTelemetryUsed(endpoint)) { | ||||
|             return; | ||||
| @@ -352,21 +334,16 @@ export default class ConditionManager extends EventEmitter { | ||||
|         const normalizedDatum = this.createNormalizedDatum(datum, endpoint); | ||||
|         const timeSystemKey = this.openmct.time.timeSystem().key; | ||||
|         let timestamp = {}; | ||||
|         const currentTimestamp = normalizedDatum[timeSystemKey]; | ||||
|         timestamp[timeSystemKey] = currentTimestamp; | ||||
|         if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { | ||||
|             this.updateConditionResults(normalizedDatum); | ||||
|             this.updateCurrentCondition(timestamp); | ||||
|         } | ||||
|     } | ||||
|         timestamp[timeSystemKey] = normalizedDatum[timeSystemKey]; | ||||
|  | ||||
|     updateConditionResults(normalizedDatum) { | ||||
|         //We want to stop when the first condition evaluates to true. | ||||
|         this.conditions.some((condition) => { | ||||
|             condition.updateResult(normalizedDatum); | ||||
|  | ||||
|             return condition.result === true; | ||||
|         }); | ||||
|  | ||||
|         this.updateCurrentCondition(timestamp); | ||||
|     } | ||||
|  | ||||
|     updateCurrentCondition(timestamp) { | ||||
|   | ||||
| @@ -86,7 +86,6 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|     updateObjectStyleConfig(styleConfiguration) { | ||||
|         if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { | ||||
|             this.initialize(styleConfiguration || {}); | ||||
|             this.applyStaticStyle(); | ||||
|             this.destroy(); | ||||
|         } else { | ||||
|             let isNewConditionSet = !this.conditionSetIdentifier | ||||
| @@ -159,6 +158,7 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.applyStaticStyle(); | ||||
|         if (this.stopProvidingTelemetry) { | ||||
|             this.stopProvidingTelemetry(); | ||||
|             delete this.stopProvidingTelemetry; | ||||
|   | ||||
| @@ -195,11 +195,11 @@ import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants"; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         Criterion, | ||||
|         ConditionDescription | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         currentConditionId: { | ||||
|             type: String, | ||||
|   | ||||
| @@ -81,10 +81,10 @@ import Condition from './Condition.vue'; | ||||
| import ConditionManager from '../ConditionManager'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     components: { | ||||
|         Condition | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         isEditing: Boolean, | ||||
|         testData: { | ||||
|   | ||||
| @@ -58,11 +58,11 @@ import TestData from './TestData.vue'; | ||||
| import ConditionCollection from './ConditionCollection.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ["openmct", "domainObject"], | ||||
|     components: { | ||||
|         TestData, | ||||
|         ConditionCollection | ||||
|     }, | ||||
|     inject: ["openmct", "domainObject"], | ||||
|     props: { | ||||
|         isEditing: Boolean | ||||
|     }, | ||||
|   | ||||
| @@ -31,6 +31,7 @@ | ||||
|             v-model="expanded" | ||||
|             class="c-tree__item__view-control" | ||||
|             :enabled="hasChildren" | ||||
|             :propagate="false" | ||||
|         /> | ||||
|         <div class="c-tree__item__label c-object-label"> | ||||
|             <div | ||||
| @@ -41,7 +42,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <ul | ||||
|         v-if="expanded && !isLoading" | ||||
|         v-if="expanded" | ||||
|         class="c-tree" | ||||
|     > | ||||
|         <li | ||||
| @@ -68,10 +69,10 @@ import viewControl from '@/ui/components/viewControl.vue'; | ||||
|  | ||||
| export default { | ||||
|     name: 'ConditionSetDialogTreeItem', | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         viewControl | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         node: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|         ></div> | ||||
|         <!-- end loading --> | ||||
|  | ||||
|         <div v-if="shouldDisplayNoResultsText" | ||||
|         <div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)" | ||||
|              class="c-tree-and-search__no-results" | ||||
|         > | ||||
|             No results found | ||||
| @@ -63,7 +63,7 @@ | ||||
|         <!-- end main tree --> | ||||
|  | ||||
|         <!-- search tree --> | ||||
|         <ul v-if="searchValue && !isLoading" | ||||
|         <ul v-if="searchValue" | ||||
|             class="c-tree-and-search__tree c-tree" | ||||
|         > | ||||
|             <condition-set-dialog-tree-item | ||||
| @@ -80,17 +80,16 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import debounce from 'lodash/debounce'; | ||||
| import search from '@/ui/components/search.vue'; | ||||
| import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     name: 'ConditionSetSelectorDialog', | ||||
|     components: { | ||||
|         search, | ||||
|         ConditionSetDialogTreeItem | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             expanded: false, | ||||
| @@ -101,20 +100,8 @@ export default { | ||||
|             selectedItem: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         shouldDisplayNoResultsText() { | ||||
|             if (this.isLoading) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return this.allTreeItems.length === 0 | ||||
|                 || (this.searchValue && this.filteredTreeItems.length === 0); | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|         this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.searchService = this.openmct.$injector.get('searchService'); | ||||
|         this.getAllChildren(); | ||||
|     }, | ||||
|     methods: { | ||||
| @@ -137,44 +124,37 @@ export default { | ||||
|                 }); | ||||
|         }, | ||||
|         getFilteredChildren() { | ||||
|             // clear any previous search results | ||||
|             this.filteredTreeItems = []; | ||||
|             this.searchService.query(this.searchValue).then(children => { | ||||
|                 this.filteredTreeItems = children.hits.map(child => { | ||||
|  | ||||
|             const promises = this.openmct.objects.search(this.searchValue) | ||||
|                 .map(promise => promise | ||||
|                     .then(results => this.aggregateFilteredChildren(results))); | ||||
|                     let context = child.object.getCapability('context'); | ||||
|                     let object = child.object.useCapability('adapter'); | ||||
|                     let objectPath = []; | ||||
|                     let navigateToParent; | ||||
|  | ||||
|             Promise.all(promises).then(() => { | ||||
|                 this.isLoading = false; | ||||
|                     if (context) { | ||||
|                         objectPath = context.getPath().slice(1) | ||||
|                             .map(oldObject => oldObject.useCapability('adapter')) | ||||
|                             .reverse(); | ||||
|                         navigateToParent = '/browse/' + objectPath.slice(1) | ||||
|                             .map((parent) => this.openmct.objects.makeKeyString(parent.identifier)) | ||||
|                             .join('/'); | ||||
|                     } | ||||
|  | ||||
|                     return { | ||||
|                         id: this.openmct.objects.makeKeyString(object.identifier), | ||||
|                         object, | ||||
|                         objectPath, | ||||
|                         navigateToParent | ||||
|                     }; | ||||
|                 }); | ||||
|             }); | ||||
|         }, | ||||
|         async aggregateFilteredChildren(results) { | ||||
|             for (const object of results) { | ||||
|                 const objectPath = await this.openmct.objects.getOriginalPath(object.identifier); | ||||
|  | ||||
|                 const navigateToParent = '/browse/' | ||||
|                     + objectPath.slice(1) | ||||
|                         .map(parent => this.openmct.objects.makeKeyString(parent.identifier)) | ||||
|                         .join('/'); | ||||
|  | ||||
|                 const filteredChild = { | ||||
|                     id: this.openmct.objects.makeKeyString(object.identifier), | ||||
|                     object, | ||||
|                     objectPath, | ||||
|                     navigateToParent | ||||
|                 }; | ||||
|  | ||||
|                 this.filteredTreeItems.push(filteredChild); | ||||
|             } | ||||
|         }, | ||||
|         searchTree(value) { | ||||
|             this.searchValue = value; | ||||
|             this.isLoading = true; | ||||
|  | ||||
|             if (this.searchValue !== '') { | ||||
|                 this.getDebouncedFilteredChildren(); | ||||
|             } else { | ||||
|                 this.isLoading = false; | ||||
|                 this.getFilteredChildren(); | ||||
|             } | ||||
|         }, | ||||
|         handleItemSelection(item, node) { | ||||
|   | ||||
| @@ -136,7 +136,7 @@ describe('the plugin', function () { | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject); | ||||
|             let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view'); | ||||
|             expect(conditionSetView).toBeDefined(); | ||||
|         }); | ||||
| @@ -401,15 +401,15 @@ describe('the plugin', function () { | ||||
|             let viewContainer = document.createElement('div'); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     StylesView | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     selection: selection, | ||||
|                     stylesManager | ||||
|                 }, | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     StylesView | ||||
|                 }, | ||||
|                 template: '<styles-view/>' | ||||
|             }); | ||||
|  | ||||
| @@ -543,6 +543,7 @@ describe('the plugin', function () { | ||||
|         }); | ||||
|  | ||||
|         it('should evaluate as stale when telemetry is not received in the allotted time', (done) => { | ||||
|  | ||||
|             let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); | ||||
|             conditionMgr.on('conditionSetResultUpdated', mockListener); | ||||
|             conditionMgr.telemetryObjects = { | ||||
| @@ -564,7 +565,7 @@ describe('the plugin', function () { | ||||
|         }); | ||||
|  | ||||
|         it('should not evaluate as stale when telemetry is received in the allotted time', (done) => { | ||||
|             const date = 1; | ||||
|             const date = Date.now(); | ||||
|             conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"]; | ||||
|             let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); | ||||
|             conditionMgr.on('conditionSetResultUpdated', mockListener); | ||||
|   | ||||
| @@ -56,14 +56,14 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|   | ||||
| @@ -51,11 +51,11 @@ export default { | ||||
|             height: 5 | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -152,7 +152,10 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let domainObject = JSON.parse(JSON.stringify(this.domainObject)); | ||||
|  | ||||
|         return { | ||||
|             internalDomainObject: domainObject, | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
| @@ -160,10 +163,10 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         gridSize() { | ||||
|             return this.domainObject.configuration.layoutGrid; | ||||
|             return this.internalDomainObject.configuration.layoutGrid; | ||||
|         }, | ||||
|         layoutItems() { | ||||
|             return this.domainObject.configuration.items; | ||||
|             return this.internalDomainObject.configuration.items; | ||||
|         }, | ||||
|         selectedLayoutItems() { | ||||
|             return this.layoutItems.filter(item => { | ||||
| @@ -171,7 +174,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         layoutDimensions() { | ||||
|             return this.domainObject.configuration.layoutDimensions; | ||||
|             return this.internalDomainObject.configuration.layoutDimensions; | ||||
|         }, | ||||
|         shouldDisplayLayoutDimensions() { | ||||
|             return this.layoutDimensions | ||||
| @@ -203,9 +206,12 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) { | ||||
|             this.internalDomainObject = JSON.parse(JSON.stringify(obj)); | ||||
|         }.bind(this)); | ||||
|         this.openmct.selection.on('change', this.setSelection); | ||||
|         this.initializeItems(); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.composition = this.openmct.composition.get(this.internalDomainObject); | ||||
|         this.composition.on('add', this.addChild); | ||||
|         this.composition.on('remove', this.removeChild); | ||||
|         this.composition.load(); | ||||
| @@ -214,6 +220,7 @@ export default { | ||||
|         this.openmct.selection.off('change', this.setSelection); | ||||
|         this.composition.off('add', this.addChild); | ||||
|         this.composition.off('remove', this.removeChild); | ||||
|         this.unlisten(); | ||||
|     }, | ||||
|     methods: { | ||||
|         addElement(itemType, element) { | ||||
| @@ -340,7 +347,7 @@ export default { | ||||
|             this.startingMinY2 = undefined; | ||||
|         }, | ||||
|         mutate(path, value) { | ||||
|             this.openmct.objects.mutate(this.domainObject, path, value); | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, path, value); | ||||
|         }, | ||||
|         handleDrop($event) { | ||||
|             if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) { | ||||
| @@ -380,11 +387,11 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         containsObject(identifier) { | ||||
|             return _.get(this.domainObject, 'composition') | ||||
|             return _.get(this.internalDomainObject, 'composition') | ||||
|                 .some(childId => this.openmct.objects.areIdsEqual(childId, identifier)); | ||||
|         }, | ||||
|         handleDragOver($event) { | ||||
|             if (this.domainObject.locked) { | ||||
|             if (this.internalDomainObject.locked) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -413,7 +420,7 @@ export default { | ||||
|             item.id = uuid(); | ||||
|             this.trackItem(item); | ||||
|             this.layoutItems.push(item); | ||||
|             this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems); | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems); | ||||
|             this.initSelectIndex = this.layoutItems.length - 1; | ||||
|         }, | ||||
|         trackItem(item) { | ||||
| @@ -470,7 +477,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         removeFromComposition(keyString) { | ||||
|             let composition = _.get(this.domainObject, 'composition'); | ||||
|             let composition = _.get(this.internalDomainObject, 'composition'); | ||||
|             composition = composition.filter(identifier => { | ||||
|                 return this.openmct.objects.makeKeyString(identifier) !== keyString; | ||||
|             }); | ||||
| @@ -622,10 +629,10 @@ export default { | ||||
|         createNewDomainObject(domainObject, composition, viewType, nameExtension, model) { | ||||
|             let identifier = { | ||||
|                 key: uuid(), | ||||
|                 namespace: this.domainObject.identifier.namespace | ||||
|                 namespace: this.internalDomainObject.identifier.namespace | ||||
|             }; | ||||
|             let type = this.openmct.types.get(viewType); | ||||
|             let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier); | ||||
|             let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name; | ||||
|             let object = {}; | ||||
|  | ||||
| @@ -682,7 +689,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         duplicateItem(selectedItems) { | ||||
|             let objectStyles = this.domainObject.configuration.objectStyles || {}; | ||||
|             let objectStyles = this.internalDomainObject.configuration.objectStyles || {}; | ||||
|             let selectItemsArray = []; | ||||
|             let newDomainObjectsArray = []; | ||||
|  | ||||
| @@ -721,8 +728,8 @@ export default { | ||||
|             }); | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems); | ||||
|                 this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles); | ||||
|                 this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems); | ||||
|                 this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles); | ||||
|                 this.$el.click(); //clear selection; | ||||
|  | ||||
|                 newDomainObjectsArray.forEach(domainObject => { | ||||
| @@ -761,13 +768,13 @@ export default { | ||||
|             }; | ||||
|             this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => { | ||||
|                 let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier); | ||||
|                 let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|                 let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier); | ||||
|  | ||||
|                 this.composition.add(newDomainObject); | ||||
|                 this.addItem('subobject-view', newDomainObject, position); | ||||
|  | ||||
|                 overlayPlots.forEach(overlayPlot => { | ||||
|                     if (overlayPlot.location === domainObjectKeyString) { | ||||
|                     if (overlayPlot.location === internalDomainObjectKeyString) { | ||||
|                         this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString); | ||||
|                     } | ||||
|                 }); | ||||
|   | ||||
| @@ -51,11 +51,11 @@ export default { | ||||
|             url: element.url | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -99,8 +99,8 @@ export default { | ||||
|             stroke: '#717171' | ||||
|         }; | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -80,11 +80,11 @@ export default { | ||||
|             viewKey | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     components: { | ||||
|         ObjectFrame, | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -109,8 +109,7 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: undefined, | ||||
|             currentObjectPath: [], | ||||
|             mutablePromise: undefined | ||||
|             currentObjectPath: [] | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
| @@ -130,31 +129,17 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.openmct.objects.supportsMutation(this.item.identifier)) { | ||||
|             this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } else { | ||||
|             this.openmct.objects.get(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } | ||||
|         this.openmct.objects.get(this.item.identifier) | ||||
|             .then(this.setObject); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|     destroyed() { | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
|         } | ||||
|  | ||||
|         if (this.mutablePromise) { | ||||
|             this.mutablePromise.then(() => { | ||||
|                 this.openmct.objects.destroyMutable(this.domainObject); | ||||
|             }); | ||||
|         } else { | ||||
|             this.openmct.objects.destroyMutable(this.domainObject); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.mutablePromise = undefined; | ||||
|             this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); | ||||
|             this.$nextTick(() => { | ||||
|                 let reference = this.$refs.objectFrame; | ||||
|   | ||||
| @@ -98,11 +98,11 @@ export default { | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -131,8 +131,7 @@ export default { | ||||
|             domainObject: undefined, | ||||
|             formats: undefined, | ||||
|             viewKey: `alphanumeric-format-${Math.random()}`, | ||||
|             status: '', | ||||
|             mutablePromise: undefined | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -213,20 +212,14 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.openmct.objects.supportsMutation(this.item.identifier)) { | ||||
|             this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } else { | ||||
|             this.openmct.objects.get(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } | ||||
|  | ||||
|         this.openmct.objects.get(this.item.identifier) | ||||
|             .then(this.setObject); | ||||
|         this.openmct.time.on("bounds", this.refreshData); | ||||
|  | ||||
|         this.status = this.openmct.status.get(this.item.identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|     destroyed() { | ||||
|         this.removeSubscription(); | ||||
|         this.removeStatusListener(); | ||||
|  | ||||
| @@ -235,14 +228,6 @@ export default { | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|  | ||||
|         if (this.mutablePromise) { | ||||
|             this.mutablePromise.then(() => { | ||||
|                 this.openmct.objects.destroyMutable(this.domainObject); | ||||
|             }); | ||||
|         } else { | ||||
|             this.openmct.objects.destroyMutable(this.domainObject); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         formattedValueForCopy() { | ||||
| @@ -301,7 +286,6 @@ export default { | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.mutablePromise = undefined; | ||||
|             this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|             this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); | ||||
|   | ||||
| @@ -59,11 +59,11 @@ export default { | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -83,7 +83,7 @@ describe('the plugin', function () { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|         const applicableViews = openmct.objectViews.get(testViewObject); | ||||
|         let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view'); | ||||
|         expect(displayLayoutViewProvider).toBeDefined(); | ||||
|     }); | ||||
|   | ||||
| @@ -57,7 +57,6 @@ describe("The Duplicate Action plugin", () => { | ||||
|             overwrite: { | ||||
|                 folder: { | ||||
|                     name: "Parent Folder", | ||||
|                     type: "folder", | ||||
|                     composition: [childObject.identifier] | ||||
|                 } | ||||
|             } | ||||
| @@ -105,7 +104,6 @@ describe("The Duplicate Action plugin", () => { | ||||
|  | ||||
|         // already installed by default, but never hurts, just adds to context menu | ||||
|         openmct.install(DuplicateActionPlugin()); | ||||
|         openmct.types.addType('folder', {creatable: true}); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|   | ||||
| @@ -47,13 +47,13 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new Vue({ | ||||
|                             provide: { | ||||
|                                 openmct | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FiltersView: FiltersView.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct | ||||
|                             }, | ||||
|                             template: '<filters-view></filters-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|   | ||||
| @@ -65,11 +65,11 @@ import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; | ||||
| import isEmpty from 'lodash/isEmpty'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FilterField, | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         filterObject: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -41,10 +41,10 @@ | ||||
| import FilterField from './FilterField.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FilterField | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         globalMetadata: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -87,12 +87,12 @@ import DropHint from './dropHint.vue'; | ||||
| const MIN_FRAME_SIZE = 5; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FrameComponent, | ||||
|         ResizeHandle, | ||||
|         DropHint | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         container: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -28,7 +28,7 @@ | ||||
|     ></div> | ||||
|  | ||||
|     <div | ||||
|         v-if="allContainersAreEmpty" | ||||
|         v-if="areAllContainersEmpty()" | ||||
|         class="c-fl__empty" | ||||
|     > | ||||
|         <span class="c-fl__empty-message">This Flexible Layout is currently empty</span> | ||||
| @@ -94,6 +94,7 @@ import Container from '../utils/container'; | ||||
| import Frame from '../utils/frame'; | ||||
| import ResizeHandle from './resizeHandle.vue'; | ||||
| import DropHint from './dropHint.vue'; | ||||
| import RemoveAction from '../../remove/RemoveAction.js'; | ||||
|  | ||||
| const MIN_CONTAINER_SIZE = 5; | ||||
|  | ||||
| @@ -139,20 +140,19 @@ function sizeToFill(items) { | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'objectPath', 'layoutObject'], | ||||
|     components: { | ||||
|         ContainerComponent, | ||||
|         ResizeHandle, | ||||
|         DropHint | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath', 'layoutObject'], | ||||
|     props: { | ||||
|         isEditing: Boolean | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: this.layoutObject, | ||||
|             newFrameLocation: [], | ||||
|             identifierMap: {} | ||||
|             newFrameLocation: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -168,30 +168,26 @@ export default { | ||||
|         }, | ||||
|         rowsLayout() { | ||||
|             return this.domainObject.configuration.rowsLayout; | ||||
|         }, | ||||
|         allContainersAreEmpty() { | ||||
|             return this.containers.every(container => container.frames.length === 0); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.buildIdentifierMap(); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.composition.on('remove', this.removeChildObject); | ||||
|         this.composition.on('add', this.addFrame); | ||||
|         this.composition.load(); | ||||
|  | ||||
|         this.RemoveAction = new RemoveAction(this.openmct); | ||||
|  | ||||
|         this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.composition.off('remove', this.removeChildObject); | ||||
|         this.composition.off('add', this.addFrame); | ||||
|  | ||||
|         this.unobserve(); | ||||
|     }, | ||||
|     methods: { | ||||
|         buildIdentifierMap() { | ||||
|             this.containers.forEach(container => { | ||||
|                 container.frames.forEach(frame => { | ||||
|                     let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); | ||||
|                     this.identifierMap[keystring] = true; | ||||
|                 }); | ||||
|             }); | ||||
|         areAllContainersEmpty() { | ||||
|             return !this.containers.filter(container => container.frames.length).length; | ||||
|         }, | ||||
|         addContainer() { | ||||
|             let container = new Container(); | ||||
| @@ -240,21 +236,16 @@ export default { | ||||
|             this.newFrameLocation = [containerIndex, insertFrameIndex]; | ||||
|         }, | ||||
|         addFrame(domainObject) { | ||||
|             let keystring = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0; | ||||
|             let container = this.containers[containerIndex]; | ||||
|             let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length; | ||||
|             let frame = new Frame(domainObject.identifier); | ||||
|  | ||||
|             if (!this.identifierMap[keystring]) { | ||||
|                 let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0; | ||||
|                 let container = this.containers[containerIndex]; | ||||
|                 let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length; | ||||
|                 let frame = new Frame(domainObject.identifier); | ||||
|             container.frames.splice(frameIndex + 1, 0, frame); | ||||
|             sizeItems(container.frames, frame); | ||||
|  | ||||
|                 container.frames.splice(frameIndex + 1, 0, frame); | ||||
|                 sizeItems(container.frames, frame); | ||||
|  | ||||
|                 this.newFrameLocation = []; | ||||
|                 this.persist(containerIndex); | ||||
|                 this.identifierMap[keystring] = true; | ||||
|             } | ||||
|             this.newFrameLocation = []; | ||||
|             this.persist(containerIndex); | ||||
|         }, | ||||
|         deleteFrame(frameId) { | ||||
|             let container = this.containers | ||||
| @@ -263,15 +254,16 @@ export default { | ||||
|                 .frames | ||||
|                 .filter((f => f.id === frameId))[0]; | ||||
|  | ||||
|             this.removeFromComposition(frame.domainObjectIdentifier); | ||||
|  | ||||
|             this.$nextTick().then(() => { | ||||
|                 sizeToFill(container.frames); | ||||
|                 this.setSelectionToParent(); | ||||
|             }); | ||||
|             this.removeFromComposition(frame.domainObjectIdentifier) | ||||
|                 .then(() => { | ||||
|                     sizeToFill(container.frames); | ||||
|                     this.setSelectionToParent(); | ||||
|                 }); | ||||
|         }, | ||||
|         removeFromComposition(identifier) { | ||||
|             this.composition.remove({identifier}); | ||||
|             return this.openmct.objects.get(identifier).then((childDomainObject) => { | ||||
|                 this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject); | ||||
|             }); | ||||
|         }, | ||||
|         setSelectionToParent() { | ||||
|             this.$el.click(); | ||||
| @@ -350,9 +342,6 @@ export default { | ||||
|         removeChildObject(identifier) { | ||||
|             let removeIdentifier = this.openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|             this.identifierMap[removeIdentifier] = undefined; | ||||
|             delete this.identifierMap[removeIdentifier]; | ||||
|  | ||||
|             this.containers.forEach(container => { | ||||
|                 container.frames = container.frames.filter(frame => { | ||||
|                     let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); | ||||
|   | ||||
| @@ -58,10 +58,10 @@ | ||||
| import ObjectFrame from '../../../ui/components/ObjectFrame.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         ObjectFrame | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         frame: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -44,15 +44,15 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element, isEditing) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FlexibleLayoutComponent: FlexibleLayoutComponent.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath, | ||||
|                                 layoutObject: domainObject | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FlexibleLayoutComponent: FlexibleLayoutComponent.default | ||||
|                             }, | ||||
|                             data() { | ||||
|                                 return { | ||||
|                                     isEditing: isEditing | ||||
|   | ||||
| @@ -22,22 +22,18 @@ | ||||
|  | ||||
| define([ | ||||
|     './components/GridView.vue', | ||||
|     './constants.js', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     GridViewComponent, | ||||
|     constants, | ||||
|     Vue | ||||
| ) { | ||||
|     function FolderGridView(openmct) { | ||||
|         const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; | ||||
|  | ||||
|         return { | ||||
|             key: 'grid', | ||||
|             name: 'Grid View', | ||||
|             cssClass: 'icon-thumbs-strip', | ||||
|             canView: function (domainObject) { | ||||
|                 return ALLOWED_FOLDER_TYPES.includes(domainObject.type); | ||||
|                 return domainObject.type === 'folder'; | ||||
|             }, | ||||
|             view: function (domainObject) { | ||||
|                 let component; | ||||
|   | ||||
| @@ -22,24 +22,20 @@ | ||||
|  | ||||
| define([ | ||||
|     './components/ListView.vue', | ||||
|     './constants.js', | ||||
|     'vue', | ||||
|     'moment' | ||||
| ], function ( | ||||
|     ListViewComponent, | ||||
|     constants, | ||||
|     Vue, | ||||
|     Moment | ||||
| ) { | ||||
|     function FolderListView(openmct) { | ||||
|         const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; | ||||
|  | ||||
|         return { | ||||
|             key: 'list-view', | ||||
|             name: 'List View', | ||||
|             cssClass: 'icon-list-view', | ||||
|             canView: function (domainObject) { | ||||
|                 return ALLOWED_FOLDER_TYPES.includes(domainObject.type); | ||||
|                 return domainObject.type === 'folder'; | ||||
|             }, | ||||
|             view: function (domainObject) { | ||||
|                 let component; | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| <template> | ||||
| <tr | ||||
|     class="c-list-item" | ||||
|     :class="{ | ||||
|         'is-alias': item.isAlias === true | ||||
|     }" | ||||
|     :class="{ 'is-alias': item.isAlias === true }" | ||||
|     @click="navigate" | ||||
| > | ||||
|     <td class="c-list-item__name"> | ||||
|   | ||||
| @@ -1,73 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("the plugin", () => { | ||||
|     let openmct; | ||||
|     let goToFolderAction; | ||||
|     let mockObjectPath; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         goToFolderAction = openmct.actions._allActions.goToOriginal; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('installs the go to folder action', () => { | ||||
|         expect(goToFolderAction).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe('when invoked', () => { | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             mockObjectPath = [{ | ||||
|                 name: 'mock folder', | ||||
|                 type: 'folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }]; | ||||
|             spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({ | ||||
|                 identifier: { | ||||
|                     namespace: '', | ||||
|                     key: 'test' | ||||
|                 } | ||||
|             })); | ||||
|             goToFolderAction.invoke(mockObjectPath); | ||||
|         }); | ||||
|  | ||||
|         it('goes to the original location', () => { | ||||
|             expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,25 +1,3 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ImageryViewLayout from './components/ImageryViewLayout.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
|   | ||||
| @@ -1,131 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-compass" | ||||
|     :style="compassDimensionsStyle" | ||||
| > | ||||
|     <CompassHUD | ||||
|         v-if="hasCameraFieldOfView" | ||||
|         :heading="heading" | ||||
|         :sun-heading="sunHeading" | ||||
|         :camera-angle-of-view="cameraAngleOfView" | ||||
|         :camera-pan="cameraPan" | ||||
|     /> | ||||
|     <CompassRose | ||||
|         v-if="hasCameraFieldOfView" | ||||
|         :heading="heading" | ||||
|         :sun-heading="sunHeading" | ||||
|         :camera-angle-of-view="cameraAngleOfView" | ||||
|         :camera-pan="cameraPan" | ||||
|         :lock-compass="lockCompass" | ||||
|         @toggle-lock-compass="toggleLockCompass" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import CompassHUD from './CompassHUD.vue'; | ||||
| import CompassRose from './CompassRose.vue'; | ||||
|  | ||||
| const CAMERA_ANGLE_OF_VIEW = 70; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         CompassHUD, | ||||
|         CompassRose | ||||
|     }, | ||||
|     props: { | ||||
|         containerWidth: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         containerHeight: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         naturalAspectRatio: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         image: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         lockCompass: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         hasCameraFieldOfView() { | ||||
|             return this.heading !== undefined && this.cameraPan !== undefined; | ||||
|         }, | ||||
|         // compass direction from north in degrees | ||||
|         heading() { | ||||
|             return this.image.heading; | ||||
|         }, | ||||
|         pitch() { | ||||
|             return this.image.pitch; | ||||
|         }, | ||||
|         // compass direction from north in degrees | ||||
|         sunHeading() { | ||||
|             return this.image.sunOrientation; | ||||
|         }, | ||||
|         // relative direction from heading in degrees | ||||
|         cameraPan() { | ||||
|             return this.image.cameraPan; | ||||
|         }, | ||||
|         cameraTilt() { | ||||
|             return this.image.cameraTilt; | ||||
|         }, | ||||
|         cameraAngleOfView() { | ||||
|             return CAMERA_ANGLE_OF_VIEW; | ||||
|         }, | ||||
|         compassDimensionsStyle() { | ||||
|             const containerAspectRatio = this.containerWidth / this.containerHeight; | ||||
|  | ||||
|             let width; | ||||
|             let height; | ||||
|  | ||||
|             if (containerAspectRatio < this.naturalAspectRatio) { | ||||
|                 width = '100%'; | ||||
|                 height = `${ this.containerWidth / this.naturalAspectRatio }px`; | ||||
|             } else { | ||||
|                 width = `${ this.containerHeight * this.naturalAspectRatio }px`; | ||||
|                 height = '100%'; | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 width: width, | ||||
|                 height: height | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         toggleLockCompass() { | ||||
|             this.$emit('toggle-lock-compass'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,145 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-compass__hud c-hud" | ||||
| > | ||||
|     <div | ||||
|         v-for="point in visibleCompassPoints" | ||||
|         :key="point.direction" | ||||
|         :class="point.class" | ||||
|         :style="point.style" | ||||
|     > | ||||
|         {{ point.direction }} | ||||
|     </div> | ||||
|     <div | ||||
|         v-if="isSunInRange" | ||||
|         ref="sun" | ||||
|         class="c-hud__sun" | ||||
|         :style="sunPositionStyle" | ||||
|     ></div> | ||||
|     <div class="c-hud__range"></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { | ||||
|     rotate, | ||||
|     inRange, | ||||
|     percentOfRange | ||||
| } from './utils'; | ||||
|  | ||||
| const COMPASS_POINTS = [ | ||||
|     { | ||||
|         direction: 'N', | ||||
|         class: 'c-hud__dir', | ||||
|         degrees: 0 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'NE', | ||||
|         class: 'c-hud__dir--sub', | ||||
|         degrees: 45 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'E', | ||||
|         class: 'c-hud__dir', | ||||
|         degrees: 90 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'SE', | ||||
|         class: 'c-hud__dir--sub', | ||||
|         degrees: 135 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'S', | ||||
|         class: 'c-hud__dir', | ||||
|         degrees: 180 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'SW', | ||||
|         class: 'c-hud__dir--sub', | ||||
|         degrees: 225 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'W', | ||||
|         class: 'c-hud__dir', | ||||
|         degrees: 270 | ||||
|     }, | ||||
|     { | ||||
|         direction: 'NW', | ||||
|         class: 'c-hud__dir--sub', | ||||
|         degrees: 315 | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         heading: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         sunHeading: { | ||||
|             type: Number, | ||||
|             default: undefined | ||||
|         }, | ||||
|         cameraAngleOfView: { | ||||
|             type: Number, | ||||
|             default: undefined | ||||
|         }, | ||||
|         cameraPan: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         visibleCompassPoints() { | ||||
|             return COMPASS_POINTS | ||||
|                 .filter(point => inRange(point.degrees, this.visibleRange)) | ||||
|                 .map(point => { | ||||
|                     const percentage = percentOfRange(point.degrees, this.visibleRange); | ||||
|                     point.style = Object.assign( | ||||
|                         { left: `${ percentage * 100 }%` } | ||||
|                     ); | ||||
|  | ||||
|                     return point; | ||||
|                 }); | ||||
|         }, | ||||
|         isSunInRange() { | ||||
|             return inRange(this.sunHeading, this.visibleRange); | ||||
|         }, | ||||
|         sunPositionStyle() { | ||||
|             const percentage = percentOfRange(this.sunHeading, this.visibleRange); | ||||
|  | ||||
|             return { | ||||
|                 left: `${ percentage * 100 }%` | ||||
|             }; | ||||
|         }, | ||||
|         visibleRange() { | ||||
|             return [ | ||||
|                 rotate(this.heading, this.cameraPan, -this.cameraAngleOfView / 2), | ||||
|                 rotate(this.heading, this.cameraPan, this.cameraAngleOfView / 2) | ||||
|             ]; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,267 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-direction-rose" | ||||
|     @click="toggleLockCompass" | ||||
| > | ||||
|     <div | ||||
|         class="c-nsew" | ||||
|         :style="rotateFrameStyle" | ||||
|     > | ||||
|         <svg | ||||
|             class="c-nsew__minor-ticks" | ||||
|             viewBox="0 0 100 100" | ||||
|         > | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-ne" | ||||
|                 x="49" | ||||
|                 y="0" | ||||
|                 width="2" | ||||
|                 height="5" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-se" | ||||
|                 x="95" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-sw" | ||||
|                 x="49" | ||||
|                 y="95" | ||||
|                 width="2" | ||||
|                 height="5" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-nw" | ||||
|                 x="0" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|  | ||||
|         </svg> | ||||
|  | ||||
|         <svg | ||||
|             class="c-nsew__ticks" | ||||
|             viewBox="0 0 100 100" | ||||
|         > | ||||
|             <polygon | ||||
|                 class="c-nsew__tick c-tick-n" | ||||
|                 points="50,0 57,5 43,5" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-e" | ||||
|                 x="95" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-w" | ||||
|                 x="0" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-s" | ||||
|                 x="49" | ||||
|                 y="95" | ||||
|                 width="2" | ||||
|                 height="5" | ||||
|             /> | ||||
|  | ||||
|             <text | ||||
|                 class="c-nsew__label c-label-n" | ||||
|                 text-anchor="middle" | ||||
|                 :transform="northTextTransform" | ||||
|             >N</text> | ||||
|             <text | ||||
|                 class="c-nsew__label c-label-e" | ||||
|                 text-anchor="middle" | ||||
|                 :transform="eastTextTransform" | ||||
|             >E</text> | ||||
|             <text | ||||
|                 class="c-nsew__label c-label-w" | ||||
|                 text-anchor="middle" | ||||
|                 :transform="southTextTransform" | ||||
|             >W</text> | ||||
|             <text | ||||
|                 class="c-nsew__label c-label-s" | ||||
|                 text-anchor="middle" | ||||
|                 :transform="westTextTransform" | ||||
|             >S</text> | ||||
|         </svg> | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|         class="c-spacecraft-body" | ||||
|         :style="headingStyle" | ||||
|     > | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|         v-if="showSunHeading" | ||||
|         class="c-sun" | ||||
|         :style="sunHeadingStyle" | ||||
|     ></div> | ||||
|  | ||||
|     <div | ||||
|         v-if="showCameraFOV" | ||||
|         class="c-cam-field" | ||||
|         :style="cameraHeadingStyle" | ||||
|     > | ||||
|         <div class="cam-field-half cam-field-half-l"> | ||||
|             <div | ||||
|                 class="cam-field-area" | ||||
|                 :style="cameraFOVStyleLeftHalf" | ||||
|             ></div> | ||||
|         </div> | ||||
|         <div class="cam-field-half cam-field-half-r"> | ||||
|             <div | ||||
|                 class="cam-field-area" | ||||
|                 :style="cameraFOVStyleRightHalf" | ||||
|             ></div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { rotate } from './utils'; | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         heading: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         sunHeading: { | ||||
|             type: Number, | ||||
|             default: undefined | ||||
|         }, | ||||
|         cameraAngleOfView: { | ||||
|             type: Number, | ||||
|             default: undefined | ||||
|         }, | ||||
|         cameraPan: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         lockCompass: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         cameraHeading() { | ||||
|             return rotate(this.heading, this.cameraPan); | ||||
|         }, | ||||
|         compassHeading() { | ||||
|             return this.lockCompass ? this.cameraHeading : 0; | ||||
|         }, | ||||
|         north() { | ||||
|             return rotate(this.compassHeading, -this.cameraHeading); | ||||
|         }, | ||||
|         rotateFrameStyle() { | ||||
|             return { transform: `rotate(${ this.north }deg)` }; | ||||
|         }, | ||||
|         northTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.north; | ||||
|         }, | ||||
|         eastTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.east; | ||||
|         }, | ||||
|         southTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.south; | ||||
|         }, | ||||
|         westTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.west; | ||||
|         }, | ||||
|         cardinalPointsTextTransform() { | ||||
|             /** | ||||
|              * cardinal points text must be rotated | ||||
|              * in the opposite direction that north is rotated | ||||
|              * to keep text vertically oriented | ||||
|              */ | ||||
|             const rotation = `rotate(${ -this.north })`; | ||||
|  | ||||
|             return { | ||||
|                 north: `translate(50,15) ${ rotation }`, | ||||
|                 east: `translate(87,50) ${ rotation }`, | ||||
|                 south: `translate(13,50) ${ rotation }`, | ||||
|                 west: `translate(50,87) ${ rotation }` | ||||
|             }; | ||||
|         }, | ||||
|         headingStyle() { | ||||
|             const rotation = rotate(this.north, this.heading); | ||||
|  | ||||
|             return { | ||||
|                 transform: `translateX(-50%) rotate(${ rotation }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         cameraHeadingStyle() { | ||||
|             const rotation = rotate(this.north, this.cameraHeading); | ||||
|  | ||||
|             return { | ||||
|                 transform: `rotate(${ rotation }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         showSunHeading() { | ||||
|             return this.sunHeading !== undefined; | ||||
|         }, | ||||
|         sunHeadingStyle() { | ||||
|             const rotation = rotate(this.north, this.sunHeading); | ||||
|  | ||||
|             return { | ||||
|                 transform: `rotate(${ rotation }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         showCameraFOV() { | ||||
|             return this.cameraPan !== undefined && this.cameraAngleOfView > 0; | ||||
|         }, | ||||
|         // left half of camera field of view | ||||
|         // rotated counter-clockwise from camera field of view heading | ||||
|         cameraFOVStyleLeftHalf() { | ||||
|             return { | ||||
|                 transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         // right half of camera field of view | ||||
|         // rotated clockwise from camera field of view heading | ||||
|         cameraFOVStyleRightHalf() { | ||||
|             return { | ||||
|                 transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)` | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         toggleLockCompass() { | ||||
|             this.$emit('toggle-lock-compass'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,214 +0,0 @@ | ||||
| /***************************** THEME/UI CONSTANTS AND MIXINS */ | ||||
| $interfaceKeyColor: #00B9C5; | ||||
| $elemBg: rgba(black, 0.7); | ||||
|  | ||||
| @mixin sun($position: 'circle closest-side') { | ||||
|     $color: #ff9900; | ||||
|     $gradEdgePerc: 60%; | ||||
|     background: radial-gradient(#{$position}, $color, $color $gradEdgePerc, rgba($color, 0.4) $gradEdgePerc + 5%, transparent); | ||||
|  | ||||
| } | ||||
|  | ||||
| .c-compass { | ||||
|     position: absolute; | ||||
|     left: 50%; | ||||
|     top: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
|     z-index: 1; | ||||
|     @include userSelectNone; | ||||
| } | ||||
|  | ||||
| /***************************** COMPASS HUD */ | ||||
| .c-hud { | ||||
|   // To be placed within a imagery view, in the bounding box of the image | ||||
|   $m: 1px; | ||||
|   $padTB: 2px; | ||||
|   $padLR: $padTB; | ||||
|   color: $interfaceKeyColor; | ||||
|   font-size: 0.8em; | ||||
|   position: absolute; | ||||
|   top: $m; right: $m; left: $m; | ||||
|   height: 18px; | ||||
|  | ||||
|   svg, div { | ||||
|     position: absolute; | ||||
|   } | ||||
|  | ||||
|   &__display { | ||||
|       height: 30px; | ||||
|       pointer-events: all; | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       left: 0; | ||||
|   } | ||||
|  | ||||
|   &__range { | ||||
|     border: 1px solid $interfaceKeyColor; | ||||
|     border-top-color: transparent; | ||||
|     position: absolute; | ||||
|     top: 50%; right: $padLR; bottom: $padTB; left: $padLR; | ||||
|   } | ||||
|  | ||||
|   [class*="__dir"] { | ||||
|     // NSEW | ||||
|     display: inline-block; | ||||
|     font-weight: bold; | ||||
|     text-shadow: 0 1px 2px black; | ||||
|     top: 50%; | ||||
|     transform: translate(-50%,-50%); | ||||
|     z-index: 2; | ||||
|   } | ||||
|  | ||||
|   [class*="__dir--sub"] { | ||||
|     font-weight: normal; | ||||
|     opacity: 0.5; | ||||
|   } | ||||
|  | ||||
|   &__sun { | ||||
|     $s: 10px; | ||||
|     @include sun('circle farthest-side at bottom'); | ||||
|     bottom: $padTB + 2px; | ||||
|     height: $s; width: $s*2; | ||||
|     opacity: 0.8; | ||||
|     transform: translateX(-50%); | ||||
|     z-index: 1; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| /***************************** COMPASS DIRECTIONS */ | ||||
| .c-nsew { | ||||
|   $color: $interfaceKeyColor; | ||||
|   $inset: 7%; | ||||
|   $tickHeightPerc: 15%; | ||||
|   text-shadow: black 0 0 10px; | ||||
|   top: $inset; right: $inset; bottom: $inset; left: $inset; | ||||
|   z-index: 3; | ||||
|  | ||||
|   &__tick, | ||||
|   &__label { | ||||
|     fill: $color; | ||||
|   } | ||||
|  | ||||
|   &__minor-ticks { | ||||
|     opacity: 0.5; | ||||
|     transform-origin: center; | ||||
|     transform: rotate(45deg); | ||||
|   } | ||||
|  | ||||
|   &__label { | ||||
|     dominant-baseline: central; | ||||
|     font-size: 0.8em; | ||||
|     font-weight: bold; | ||||
|   } | ||||
|  | ||||
|   .c-label-n { | ||||
|     font-size: 1.1em; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /***************************** CAMERA FIELD ANGLE */ | ||||
| .c-cam-field { | ||||
|   $color: white; | ||||
|   opacity: 0.2; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   z-index: 2; | ||||
|  | ||||
|   .cam-field-half { | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|  | ||||
|     .cam-field-area { | ||||
|       background: $color; | ||||
|       top: -30%; | ||||
|       right: 0; | ||||
|       bottom: -30%; | ||||
|       left: 0; | ||||
|     } | ||||
|  | ||||
|     // clip-paths overlap a bit to avoid a gap between halves | ||||
|     &-l { | ||||
|       clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%); | ||||
|       .cam-field-area { | ||||
|         transform-origin: left center; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &-r { | ||||
|       clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%); | ||||
|       .cam-field-area { | ||||
|         transform-origin: right center; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /***************************** SPACECRAFT BODY */ | ||||
| .c-spacecraft-body { | ||||
|   $color: $interfaceKeyColor; | ||||
|   $s: 30%; | ||||
|   background: $color; | ||||
|   border-radius: 3px; | ||||
|   height: $s; width: $s; | ||||
|   left: 50%; top: 50%; | ||||
|   opacity: 0.4; | ||||
|   transform-origin: center top; | ||||
|  | ||||
|   &:before { | ||||
|     // Direction arrow | ||||
|     $color: rgba(black, 0.5); | ||||
|     $arwPointerY: 60%; | ||||
|     $arwBodyOffset: 25%; | ||||
|     background: $color; | ||||
|     content: ''; | ||||
|     display: block; | ||||
|     position: absolute; | ||||
|     top: 10%; right: 20%; bottom: 50%; left: 20%; | ||||
|     clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /***************************** DIRECTION ROSE */ | ||||
| .c-direction-rose { | ||||
|   $d: 100px; | ||||
|   $c2: rgba(white, 0.1); | ||||
|   background: $elemBg; | ||||
|   background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2); | ||||
|   width: $d; | ||||
|   height: $d; | ||||
|   transform-origin: 0 0; | ||||
|   position: absolute; | ||||
|   bottom: 10px; left: 10px; | ||||
|   clip-path: circle(50% at 50% 50%); | ||||
|   border-radius: 100%; | ||||
|  | ||||
|   svg, div { | ||||
|     position: absolute; | ||||
|   } | ||||
|  | ||||
|   // Sun | ||||
|   .c-sun { | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|  | ||||
|     &:before { | ||||
|       $s: 35%; | ||||
|       @include sun(); | ||||
|       content: ''; | ||||
|       display: block; | ||||
|       position: absolute; | ||||
|       opacity: 0.7; | ||||
|       top: 0; left: 50%; | ||||
|       height:$s; width: $s; | ||||
|       transform: translate(-50%, -60%); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import Compass from './Compass.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| const COMPASS_ROSE_CLASS = '.c-direction-rose'; | ||||
| const COMPASS_HUD_CLASS = '.c-compass__hud'; | ||||
|  | ||||
| describe("The Compass component", () => { | ||||
|     let app; | ||||
|     let instance; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         let imageDatum = { | ||||
|             heading: 100, | ||||
|             roll: 90, | ||||
|             pitch: 90, | ||||
|             cameraTilt: 100, | ||||
|             cameraPan: 90, | ||||
|             sunAngle: 30 | ||||
|         }; | ||||
|         let propsData = { | ||||
|             containerWidth: 600, | ||||
|             containerHeight: 600, | ||||
|             naturalAspectRatio: 0.9, | ||||
|             image: imageDatum | ||||
|         }; | ||||
|  | ||||
|         app = new Vue({ | ||||
|             components: { Compass }, | ||||
|             data() { | ||||
|                 return propsData; | ||||
|             }, | ||||
|             template: `<Compass | ||||
|                 :container-width="containerWidth" | ||||
|                 :container-height="containerHeight" | ||||
|                 :natural-aspect-ratio="naturalAspectRatio" | ||||
|                 :image="image" />` | ||||
|         }); | ||||
|         instance = app.$mount(); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     afterAll(() => { | ||||
|         app.$destroy(); | ||||
|     }); | ||||
|  | ||||
|     describe("when a heading value exists on the image", () => { | ||||
|  | ||||
|         it("should display a compass rose", () => { | ||||
|             let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS | ||||
|             ); | ||||
|  | ||||
|             expect(compassRoseElement).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("should display a compass HUD", () => { | ||||
|             let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS); | ||||
|  | ||||
|             expect(compassHUDElement).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -1,33 +0,0 @@ | ||||
| export function rotate(direction, ...rotations) { | ||||
|     const rotation = rotations.reduce((a, b) => a + b, 0); | ||||
|  | ||||
|     return normalizeCompassDirection(direction + rotation); | ||||
| } | ||||
|  | ||||
| export function normalizeCompassDirection(degrees) { | ||||
|     const base = degrees % 360; | ||||
|  | ||||
|     return base >= 0 ? base : 360 + base; | ||||
| } | ||||
|  | ||||
| export function inRange(degrees, [min, max]) { | ||||
|     return min > max | ||||
|         ? (degrees >= min && degrees < 360) || (degrees <= max && degrees >= 0) | ||||
|         : degrees >= min && degrees <= max; | ||||
| } | ||||
|  | ||||
| export function percentOfRange(degrees, [min, max]) { | ||||
|     let distance = degrees; | ||||
|     let minRange = min; | ||||
|     let maxRange = max; | ||||
|  | ||||
|     if (min > max) { | ||||
|         if (distance < max) { | ||||
|             distance += 360; | ||||
|         } | ||||
|  | ||||
|         maxRange += 360; | ||||
|     } | ||||
|  | ||||
|     return (distance - minRange) / (maxRange - minRange); | ||||
| } | ||||
| @@ -1,25 +1,3 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     tabindex="0" | ||||
| @@ -58,25 +36,14 @@ | ||||
|         <div class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|         > | ||||
|             <img | ||||
|                 ref="focusedImage" | ||||
|                 class="c-imagery__main-image__image js-imageryView-image" | ||||
|                 :src="imageUrl" | ||||
|                 :style="{ | ||||
|                     'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
|                 }" | ||||
|                 :data-openmct-image-timestamp="time" | ||||
|                 :data-openmct-object-keystring="keyString" | ||||
|             > | ||||
|             <Compass | ||||
|                 v-if="shouldDisplayCompass" | ||||
|                 :container-width="imageContainerWidth" | ||||
|                 :container-height="imageContainerHeight" | ||||
|                 :natural-aspect-ratio="focusedImageNaturalAspectRatio" | ||||
|                 :image="focusedImage" | ||||
|                 :lock-compass="lockCompass" | ||||
|                 @toggle-lock-compass="toggleLockCompass" | ||||
|             /> | ||||
|             <div class="c-imagery__main-image__image js-imageryView-image" | ||||
|                  :style="{ | ||||
|                      'background-image': imageUrl ? `url(${imageUrl})` : 'none', | ||||
|                      'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
|                  }" | ||||
|                  :data-openmct-image-timestamp="time" | ||||
|                  :data-openmct-object-keystring="keyString" | ||||
|             ></div> | ||||
|         </div> | ||||
|         <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> | ||||
|             <button class="c-nav c-nav--prev" | ||||
| @@ -94,25 +61,11 @@ | ||||
|         <div class="c-imagery__control-bar"> | ||||
|             <div class="c-imagery__time"> | ||||
|                 <div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div> | ||||
|  | ||||
|                 <!-- image fresh --> | ||||
|                 <div | ||||
|                     v-if="canTrackDuration" | ||||
|                     :class="{'c-imagery--new': isImageNew && !refreshCSS}" | ||||
|                     class="c-imagery__age icon-timer" | ||||
|                 >{{ formattedDuration }}</div> | ||||
|  | ||||
|                 <!-- spacecraft position fresh --> | ||||
|                 <div | ||||
|                     v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh" | ||||
|                     class="c-imagery__age icon-check c-imagery--new" | ||||
|                 >POS</div> | ||||
|  | ||||
|                 <!-- camera position fresh --> | ||||
|                 <div | ||||
|                     v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh" | ||||
|                     class="c-imagery__age icon-check c-imagery--new" | ||||
|                 >CAM</div> | ||||
|             </div> | ||||
|             <div class="h-local-controls"> | ||||
|                 <button | ||||
| @@ -123,32 +76,28 @@ | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div | ||||
|         ref="thumbsWrapper" | ||||
|         class="c-imagery__thumbs-wrapper" | ||||
|         :class="{'is-paused': isPaused}" | ||||
|         @scroll="handleScroll" | ||||
|     <div ref="thumbsWrapper" | ||||
|          class="c-imagery__thumbs-wrapper" | ||||
|          :class="{'is-paused': isPaused}" | ||||
|          @scroll="handleScroll" | ||||
|     > | ||||
|         <div v-for="(image, index) in imageHistory" | ||||
|              :key="image.url + image.time" | ||||
|         <div v-for="(datum, index) in imageHistory" | ||||
|              :key="datum.url" | ||||
|              class="c-imagery__thumb c-thumb" | ||||
|              :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|              @click="setFocusedImage(index, thumbnailClick)" | ||||
|         > | ||||
|             <img class="c-thumb__image" | ||||
|                  :src="image.url" | ||||
|                  :src="formatImageUrl(datum)" | ||||
|             > | ||||
|             <div class="c-thumb__timestamp">{{ image.formattedTime }}</div> | ||||
|             <div class="c-thumb__timestamp">{{ formatTime(datum) }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from 'lodash'; | ||||
| import moment from 'moment'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
| import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| @@ -167,9 +116,6 @@ const ARROW_RIGHT = 39; | ||||
| const ARROW_LEFT = 37; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Compass | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
| @@ -191,15 +137,8 @@ export default { | ||||
|             refreshCSS: false, | ||||
|             keyString: undefined, | ||||
|             focusedImageIndex: undefined, | ||||
|             focusedImageRelatedTelemetry: {}, | ||||
|             numericDuration: undefined, | ||||
|             metadataEndpoints: {}, | ||||
|             relatedTelemetry: {}, | ||||
|             latestRelatedTelemetry: {}, | ||||
|             focusedImageNaturalAspectRatio: undefined, | ||||
|             imageContainerWidth: undefined, | ||||
|             imageContainerHeight: undefined, | ||||
|             lockCompass: true | ||||
|             telemetryCollection: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -257,83 +196,15 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|         shouldDisplayCompass() { | ||||
|             return this.focusedImage !== undefined | ||||
|                 && this.focusedImageNaturalAspectRatio !== undefined | ||||
|                 && this.imageContainerWidth !== undefined | ||||
|                 && this.imageContainerHeight !== undefined; | ||||
|         }, | ||||
|         isSpacecraftPositionFresh() { | ||||
|             let isFresh = undefined; | ||||
|             let latest = this.latestRelatedTelemetry; | ||||
|             let focused = this.focusedImageRelatedTelemetry; | ||||
|  | ||||
|             if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|                 isFresh = true; | ||||
|                 for (let key of this.spacecraftPositionKeys) { | ||||
|                     if (this.relatedTelemetry[key] && latest[key] && focused[key]) { | ||||
|                         isFresh = Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])); | ||||
|                     } else { | ||||
|                         isFresh = false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return isFresh; | ||||
|         }, | ||||
|         isSpacecraftOrientationFresh() { | ||||
|             let isFresh = undefined; | ||||
|             let latest = this.latestRelatedTelemetry; | ||||
|             let focused = this.focusedImageRelatedTelemetry; | ||||
|  | ||||
|             if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|                 isFresh = true; | ||||
|                 for (let key of this.spacecraftOrientationKeys) { | ||||
|                     if (this.relatedTelemetry[key] && latest[key] && focused[key]) { | ||||
|                         isFresh = Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])); | ||||
|                     } else { | ||||
|                         isFresh = false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return isFresh; | ||||
|         }, | ||||
|         isCameraPositionFresh() { | ||||
|             let isFresh = undefined; | ||||
|             let latest = this.latestRelatedTelemetry; | ||||
|             let focused = this.focusedImageRelatedTelemetry; | ||||
|  | ||||
|             if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|                 isFresh = true; | ||||
|  | ||||
|                 // camera freshness relies on spacecraft position freshness | ||||
|                 if (this.isSpacecraftPositionFresh && this.isSpacecraftOrientationFresh) { | ||||
|                     for (let key of this.cameraKeys) { | ||||
|                         if (this.relatedTelemetry[key] && latest[key] && focused[key]) { | ||||
|                             isFresh = Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])); | ||||
|                         } else { | ||||
|                             isFresh = false; | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     isFresh = false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return isFresh; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         focusedImageIndex() { | ||||
|             this.trackDuration(); | ||||
|             this.resetAgeCSS(); | ||||
|             this.updateRelatedTelemetryForFocusedImage(); | ||||
|             this.getImageNaturalDimensions(); | ||||
|         } | ||||
|     }, | ||||
|     async mounted() { | ||||
|     mounted() { | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
| @@ -342,15 +213,8 @@ export default { | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); | ||||
|  | ||||
|         // related telemetry keys | ||||
|         this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; | ||||
|         this.spacecraftOrientationKeys = ['heading', 'roll', 'pitch']; | ||||
|         this.cameraKeys = ['cameraPan', 'cameraTilt']; | ||||
|         this.sunKeys = ['sunOrientation']; | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); | ||||
|  | ||||
|         // initialize | ||||
|         this.timeKey = this.timeSystem.key; | ||||
| @@ -359,18 +223,7 @@ export default { | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
|  | ||||
|         // related telemetry | ||||
|         await this.initializeRelatedTelemetry(); | ||||
|         this.updateRelatedTelemetryForFocusedImage(); | ||||
|         this.trackLatestRelatedTelemetry(); | ||||
|  | ||||
|         // for scrolling through images quickly and resizing the object view | ||||
|         _.debounce(this.updateRelatedTelemetryForFocusedImage, 400); | ||||
|         _.debounce(this.resizeImageContainer, 400); | ||||
|  | ||||
|         this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer); | ||||
|         this.imageContainerResizeObserver.observe(this.$refs.focusedImage); | ||||
|         this.requestTelemetry(); | ||||
|     }, | ||||
|     updated() { | ||||
|         this.scrollToRight(); | ||||
| @@ -381,115 +234,12 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.imageContainerResizeObserver.disconnect(); | ||||
|  | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             this.relatedTelemetry.destroy(); | ||||
|         } | ||||
|  | ||||
|         this.stopDurationTracking(); | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.off('clock', this.clockChange); | ||||
|  | ||||
|         // unsubscribe from related telemetry | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             for (let key of this.relatedTelemetry.keys) { | ||||
|                 if (this.relatedTelemetry[key] && this.relatedTelemetry[key].unsubscribe) { | ||||
|                     this.relatedTelemetry[key].unsubscribe(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         async initializeRelatedTelemetry() { | ||||
|             this.relatedTelemetry = new RelatedTelemetry( | ||||
|                 this.openmct, | ||||
|                 this.domainObject, | ||||
|                 [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys] | ||||
|             ); | ||||
|  | ||||
|             if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|                 await this.relatedTelemetry.load(); | ||||
|             } | ||||
|         }, | ||||
|         async getMostRecentRelatedTelemetry(key, targetDatum) { | ||||
|             if (!this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|                 throw new Error(`${this.domainObject.name} does not have any related telemetry`); | ||||
|             } | ||||
|  | ||||
|             if (!this.relatedTelemetry[key]) { | ||||
|                 throw new Error(`${key} does not exist on related telemetry`); | ||||
|             } | ||||
|  | ||||
|             let mostRecent; | ||||
|             let valueKey = this.relatedTelemetry[key].historical.valueKey; | ||||
|             let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum; | ||||
|  | ||||
|             if (valuesOnTelemetry) { | ||||
|                 mostRecent = targetDatum[valueKey]; | ||||
|  | ||||
|                 if (mostRecent) { | ||||
|                     return mostRecent; | ||||
|                 } else { | ||||
|                     console.warn(`Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`); | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum); | ||||
|  | ||||
|             return mostRecent[valueKey]; | ||||
|         }, | ||||
|         // will subscribe to data for this key if not already done | ||||
|         subscribeToDataForKey(key) { | ||||
|             if (this.relatedTelemetry[key].isSubscribed) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.relatedTelemetry[key].realtimeDomainObject) { | ||||
|                 this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe( | ||||
|                     this.relatedTelemetry[key].realtimeDomainObject, datum => { | ||||
|                         this.relatedTelemetry[key].listeners.forEach(callback => { | ||||
|                             callback(datum); | ||||
|                         }); | ||||
|  | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 this.relatedTelemetry[key].isSubscribed = true; | ||||
|             } | ||||
|         }, | ||||
|         async updateRelatedTelemetryForFocusedImage() { | ||||
|             if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // set data ON image telemetry as well as in focusedImageRelatedTelemetry | ||||
|             for (let key of this.relatedTelemetry.keys) { | ||||
|                 if (this.relatedTelemetry[key] && this.relatedTelemetry[key].historical) { | ||||
|                     let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum; | ||||
|                     let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage); | ||||
|  | ||||
|                     if (!valuesOnTelemetry) { | ||||
|                         this.$set(this.imageHistory[this.focusedImageIndex], key, value); // manually add to telemetry | ||||
|                     } | ||||
|  | ||||
|                     this.$set(this.focusedImageRelatedTelemetry, key, value); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         trackLatestRelatedTelemetry() { | ||||
|             [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => { | ||||
|                 if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) { | ||||
|                     this.relatedTelemetry[key].subscribe((datum) => { | ||||
|                         let valueKey = this.relatedTelemetry[key].realtime.valueKey; | ||||
|                         this.$set(this.latestRelatedTelemetry, key, datum[valueKey]); | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         focusElement() { | ||||
|             this.$el.focus(); | ||||
|         }, | ||||
| @@ -605,12 +355,20 @@ export default { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         async requestTelemetry() { | ||||
|             this.telemetryCollection = await this.openmct.telemetry.requestTelemetryCollection(this.domainObject); | ||||
|             this.telemetryCollection.on('add', (data) => { | ||||
|                 console.log('added data', data); | ||||
|             }); | ||||
|             this.telemetryCollection.on('remove', (data) => { | ||||
|                 console.log('removed data', data); | ||||
|             }); | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
|  | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
|  | ||||
| @@ -646,12 +404,7 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let image = { ...datum }; | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = datum[this.timeKey]; | ||||
|  | ||||
|             this.imageHistory.push(image); | ||||
|             this.imageHistory.push(datum); | ||||
|  | ||||
|             if (setFocused) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
| @@ -767,28 +520,6 @@ export default { | ||||
|         }, | ||||
|         isLeftOrRightArrowKey(keyCode) { | ||||
|             return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode); | ||||
|         }, | ||||
|         getImageNaturalDimensions() { | ||||
|             this.focusedImageNaturalAspectRatio = undefined; | ||||
|  | ||||
|             const img = this.$refs.focusedImage; | ||||
|  | ||||
|             // TODO - should probably cache this | ||||
|             img.addEventListener('load', () => { | ||||
|                 this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight; | ||||
|             }, { once: true }); | ||||
|         }, | ||||
|         resizeImageContainer() { | ||||
|             if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) { | ||||
|                 this.imageContainerWidth = this.$refs.focusedImage.clientWidth; | ||||
|             } | ||||
|  | ||||
|             if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) { | ||||
|                 this.imageContainerHeight = this.$refs.focusedImage.clientHeight; | ||||
|             } | ||||
|         }, | ||||
|         toggleLockCompass() { | ||||
|             this.lockCompass = !this.lockCompass; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -1,164 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| function copyRelatedMetadata(metadata) { | ||||
|     let compare = metadata.comparisonFunction; | ||||
|     let copiedMetadata = JSON.parse(JSON.stringify(metadata)); | ||||
|     copiedMetadata.comparisonFunction = compare; | ||||
|  | ||||
|     return copiedMetadata; | ||||
| } | ||||
|  | ||||
| export default class RelatedTelemetry { | ||||
|  | ||||
|     constructor(openmct, domainObject, telemetryKeys) { | ||||
|         this._openmct = openmct; | ||||
|         this._domainObject = domainObject; | ||||
|  | ||||
|         let metadata = this._openmct.telemetry.getMetadata(this._domainObject); | ||||
|         let imageHints = metadata.valuesForHints(['image'])[0]; | ||||
|  | ||||
|         this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined; | ||||
|  | ||||
|         if (this.hasRelatedTelemetry) { | ||||
|             this.keys = telemetryKeys; | ||||
|  | ||||
|             this._timeFormatter = undefined; | ||||
|             this._timeSystemChange(this._openmct.time.timeSystem()); | ||||
|  | ||||
|             // grab related telemetry metadata | ||||
|             for (let key of this.keys) { | ||||
|                 if (imageHints.relatedTelemetry[key]) { | ||||
|                     this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.load = this.load.bind(this); | ||||
|             this._parseTime = this._parseTime.bind(this); | ||||
|             this._timeSystemChange = this._timeSystemChange.bind(this); | ||||
|             this.destroy = this.destroy.bind(this); | ||||
|  | ||||
|             this._openmct.time.on('timeSystem', this._timeSystemChange); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async load() { | ||||
|         if (!this.hasRelatedTelemetry) { | ||||
|             throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.'); | ||||
|         } | ||||
|  | ||||
|         await Promise.all( | ||||
|             this.keys.map(async (key) => { | ||||
|                 if (this[key]) { | ||||
|                     if (this[key].historical) { | ||||
|                         await this._initializeHistorical(key); | ||||
|                     } | ||||
|  | ||||
|                     if (this[key].realtime && this[key].realtime.telemetryObjectId) { | ||||
|                         await this._intializeRealtime(key); | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     async _initializeHistorical(key) { | ||||
|         if (this[key].historical.telemetryObjectId) { | ||||
|             this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId); | ||||
|  | ||||
|             this[key].requestLatestFor = async (datum) => { | ||||
|                 const options = { | ||||
|                     start: this._openmct.time.bounds().start, | ||||
|                     end: this._parseTime(datum), | ||||
|                     strategy: 'latest' | ||||
|                 }; | ||||
|                 let results = await this._openmct.telemetry | ||||
|                     .request(this[key].historicalDomainObject, options); | ||||
|  | ||||
|                 return results[results.length - 1]; | ||||
|             }; | ||||
|         } else { | ||||
|             this[key].historical.hasTelemetryOnDatum = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async _intializeRealtime(key) { | ||||
|         this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId); | ||||
|         this[key].listeners = []; | ||||
|         this[key].subscribe = (callback) => { | ||||
|  | ||||
|             if (!this[key].isSubscribed) { | ||||
|                 this._subscribeToDataForKey(key); | ||||
|             } | ||||
|  | ||||
|             if (!this[key].listeners.includes(callback)) { | ||||
|                 this[key].listeners.push(callback); | ||||
|  | ||||
|                 return () => { | ||||
|                     this[key].listeners.remove(callback); | ||||
|                 }; | ||||
|             } else { | ||||
|                 return () => {}; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     _subscribeToDataForKey(key) { | ||||
|         if (this[key].isSubscribed) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (this[key].realtimeDomainObject) { | ||||
|             this[key].unsubscribe = this._openmct.telemetry.subscribe( | ||||
|                 this[key].realtimeDomainObject, datum => { | ||||
|                     this[key].listeners.forEach(callback => { | ||||
|                         callback(datum); | ||||
|                     }); | ||||
|  | ||||
|                 } | ||||
|             ); | ||||
|  | ||||
|             this[key].isSubscribed = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _parseTime(datum) { | ||||
|         return this._timeFormatter.parse(datum); | ||||
|     } | ||||
|  | ||||
|     _timeSystemChange(system) { | ||||
|         let key = system.key; | ||||
|         let metadata = this._openmct.telemetry.getMetadata(this._domainObject); | ||||
|         let metadataValue = metadata.value(key) || { format: key }; | ||||
|         this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this._openmct.time.off('timeSystem', this._timeSystemChange); | ||||
|         for (let key of this.keys) { | ||||
|             if (this[key] && this[key].unsubscribe) { | ||||
|                 this[key].unsubscribe(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -23,7 +23,6 @@ | ||||
|             background-color: $colorPlotBg; | ||||
|             border: 1px solid transparent; | ||||
|             flex: 1 1 auto; | ||||
|             height: 0; | ||||
|  | ||||
|             &.unnsynced{ | ||||
|                 @include sUnsynced(); | ||||
| @@ -31,9 +30,10 @@ | ||||
|         } | ||||
|  | ||||
|         &__image { | ||||
|             height: 100%; | ||||
|             width: 100%; | ||||
|             object-fit: contain; | ||||
|             @include abs(); // Safari fix | ||||
|             background-position: center; | ||||
|             background-repeat: no-repeat; | ||||
|             background-size: contain; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -71,14 +71,13 @@ | ||||
|     } | ||||
|  | ||||
|     &__age { | ||||
|         border-radius: $smallCr; | ||||
|         border-radius: $controlCr; | ||||
|         display: flex; | ||||
|         flex-shrink: 0; | ||||
|         align-items: center; | ||||
|         padding: 2px $interiorMarginSm; | ||||
|         align-items: baseline; | ||||
|         padding: 1px $interiorMarginSm; | ||||
|  | ||||
|         &:before { | ||||
|             font-size: 0.9em; | ||||
|             opacity: 0.5; | ||||
|             margin-right: $interiorMarginSm; | ||||
|         } | ||||
| @@ -87,9 +86,8 @@ | ||||
|     &--new { | ||||
|         // New imagery | ||||
|         $bgColor: $colorOk; | ||||
|         color: $colorOkFg; | ||||
|         background: rgba($bgColor, 0.5); | ||||
|         @include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0)); | ||||
|         @include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0)); | ||||
|     } | ||||
|  | ||||
|     &__thumbs-wrapper { | ||||
|   | ||||
| @@ -1,25 +1,3 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ImageryViewProvider from './ImageryViewProvider'; | ||||
|  | ||||
| export default function () { | ||||
|   | ||||
| @@ -32,25 +32,12 @@ 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]; | ||||
|     let timestamp = imageElement.dataset.openmctImageTimestamp; | ||||
|     let identifier = imageElement.dataset.openmctObjectKeystring; | ||||
|     let url = imageElement.src; | ||||
|     let url = imageElement.style.backgroundImage; | ||||
|  | ||||
|     return { | ||||
|         timestamp, | ||||
| @@ -76,8 +63,7 @@ function generateTelemetry(start, count) { | ||||
|             "name": stringRep + " Imagery", | ||||
|             "utc": start + (i * ONE_MINUTE), | ||||
|             "url": location.host + '/' + logo + '?time=' + stringRep, | ||||
|             "timeId": stringRep, | ||||
|             "value": 100 | ||||
|             "timeId": stringRep | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -119,51 +105,7 @@ describe("The Imagery View Layout", () => { | ||||
|                         "image": 1, | ||||
|                         "priority": 3 | ||||
|                     }, | ||||
|                     "source": "url", | ||||
|                     "relatedTelemetry": { | ||||
|                         "heading": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "heading", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "roll": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "roll", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "pitch": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "pitch", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "cameraPan": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "cameraPan", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "cameraTilt": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "cameraTilt", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "sunOrientation": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "sunOrientation", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     "source": "url" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Name", | ||||
| @@ -209,11 +151,6 @@ describe("The Imagery View Layout", () => { | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|  | ||||
|         imageryPlugin = new ImageryPlugin(); | ||||
| @@ -235,7 +172,7 @@ describe("The Imagery View Layout", () => { | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery view only for imagery producing objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryKey | ||||
|         ); | ||||
| @@ -265,7 +202,7 @@ describe("The Imagery View Layout", () => { | ||||
|                 end: bounds.end + 100 | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|             applicableViews = openmct.objectViews.get(imageryObject); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject); | ||||
|             imageryView.show(child); | ||||
| @@ -276,10 +213,6 @@ describe("The Imagery View Layout", () => { | ||||
|             return done(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             imageryView.destroy(); | ||||
|         }); | ||||
|  | ||||
|         it("on mount should show the the most recent image", () => { | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|   | ||||
| @@ -1,99 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from "utils/testing"; | ||||
| import InterceptorPlugin from "./plugin"; | ||||
|  | ||||
| describe('the plugin', function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     const TEST_NAMESPACE = 'test'; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         const appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(new InterceptorPlugin(openmct)); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         element.style.width = '640px'; | ||||
|         element.style.height = '480px'; | ||||
|         child = document.createElement('div'); | ||||
|         child.style.width = '640px'; | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('the missingObjectInterceptor', () => { | ||||
|         let mockProvider; | ||||
|         beforeEach(() => { | ||||
|             mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                 "get" | ||||
|             ]); | ||||
|             mockProvider.get.and.returnValue(Promise.resolve(undefined)); | ||||
|             openmct.objects.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|         }); | ||||
|  | ||||
|         it('returns missing objects', (done) => { | ||||
|             const identifier = { | ||||
|                 namespace: TEST_NAMESPACE, | ||||
|                 key: 'hello' | ||||
|             }; | ||||
|             openmct.objects.get(identifier).then((testObject) => { | ||||
|                 expect(testObject).toEqual({ | ||||
|                     identifier, | ||||
|                     type: 'unknown', | ||||
|                     name: 'Missing: test:hello' | ||||
|                 }); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('returns the My items object if not found', (done) => { | ||||
|             const identifier = { | ||||
|                 namespace: TEST_NAMESPACE, | ||||
|                 key: 'mine' | ||||
|             }; | ||||
|             openmct.objects.get(identifier).then((testObject) => { | ||||
|                 expect(testObject).toEqual({ | ||||
|                     identifier, | ||||
|                     "name": "My Items", | ||||
|                     "type": "folder", | ||||
|                     "composition": [], | ||||
|                     "location": "ROOT" | ||||
|                 }); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -1,33 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.types.addType("noneditable.folder", { | ||||
|             name: "Non-Editable Folder", | ||||
|             key: "noneditable.folder", | ||||
|             description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.", | ||||
|             cssClass: "icon-folder", | ||||
|             creatable: false | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("the plugin", () => { | ||||
|     const NON_EDITABLE_FOLDER_KEY = 'noneditable.folder'; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.NonEditableFolder()); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('adds the new non-editable folder type', () => { | ||||
|         const type = openmct.types.get(NON_EDITABLE_FOLDER_KEY); | ||||
|  | ||||
|         expect(type).toBeDefined(); | ||||
|         expect(type.definition.creatable).toBeFalse(); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -97,8 +97,7 @@ | ||||
|                                :selected-page="getSelectedPage()" | ||||
|                                :selected-section="getSelectedSection()" | ||||
|                                :read-only="false" | ||||
|                                @deleteEntry="deleteEntry" | ||||
|                                @updateEntry="updateEntry" | ||||
|                                @updateEntries="updateEntries" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -112,20 +111,19 @@ import Search from '@/ui/components/search.vue'; | ||||
| import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| import { throttle } from 'lodash'; | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'snapshotContainer'], | ||||
|     components: { | ||||
|         NotebookEntry, | ||||
|         Search, | ||||
|         SearchResults, | ||||
|         Sidebar | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject', 'snapshotContainer'], | ||||
|     data() { | ||||
|         return { | ||||
|             defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '', | ||||
| @@ -184,9 +182,7 @@ export default { | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); | ||||
|         this.formatSidebar(); | ||||
|  | ||||
|         window.addEventListener('orientationchange', this.formatSidebar); | ||||
|         window.addEventListener("hashchange", this.navigateToSectionPage, false); | ||||
|  | ||||
|         this.navigateToSectionPage(); | ||||
|     }, | ||||
| @@ -194,9 +190,6 @@ export default { | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|  | ||||
|         window.removeEventListener('orientationchange', this.formatSidebar); | ||||
|         window.removeEventListener("hashchange", this.navigateToSectionPage); | ||||
|     }, | ||||
|     updated: function () { | ||||
|         this.$nextTick(() => { | ||||
| @@ -233,49 +226,18 @@ export default { | ||||
|         createNotebookStorageObject() { | ||||
|             const notebookMeta = { | ||||
|                 name: this.internalDomainObject.name, | ||||
|                 identifier: this.internalDomainObject.identifier, | ||||
|                 link: this.getLinktoNotebook() | ||||
|                 identifier: this.internalDomainObject.identifier | ||||
|             }; | ||||
|             const page = this.getSelectedPage(); | ||||
|             const section = this.getSelectedSection(); | ||||
|  | ||||
|             return { | ||||
|                 domainObject: this.internalDomainObject, | ||||
|                 notebookMeta, | ||||
|                 page, | ||||
|                 section | ||||
|                 section, | ||||
|                 page | ||||
|             }; | ||||
|         }, | ||||
|         deleteEntry(entryId) { | ||||
|             const self = this; | ||||
|             const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage); | ||||
|             if (entryPos === -1) { | ||||
|                 this.openmct.notifications.alert('Warning: unable to delete entry'); | ||||
|                 console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const dialog = this.openmct.overlays.dialog({ | ||||
|                 iconClass: 'alert', | ||||
|                 message: 'This action will permanently delete this entry. Do you wish to continue?', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: "Ok", | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage); | ||||
|                             entries.splice(entryPos, 1); | ||||
|                             self.updateEntries(entries); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         label: "Cancel", | ||||
|                         callback: () => dialog.dismiss() | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|         }, | ||||
|         dragOver(event) { | ||||
|             event.preventDefault(); | ||||
|             event.dataTransfer.dropEffect = "copy"; | ||||
| @@ -349,20 +311,6 @@ export default { | ||||
|  | ||||
|             return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier); | ||||
|         }, | ||||
|         getLinktoNotebook() { | ||||
|             const objectPath = this.openmct.router.path; | ||||
|             const link = objectLink.computed.objectLink.call({ | ||||
|                 objectPath, | ||||
|                 openmct: this.openmct | ||||
|             }); | ||||
|  | ||||
|             const selectedSection = this.selectedSection; | ||||
|             const selectedPage = this.selectedPage; | ||||
|             const sectionId = selectedSection ? selectedSection.id : ''; | ||||
|             const pageId = selectedPage ? selectedPage.id : ''; | ||||
|  | ||||
|             return `${link}?sectionId=${sectionId}&pageId=${pageId}`; | ||||
|         }, | ||||
|         getPage(section, id) { | ||||
|             return section.pages.find(p => p.id === id); | ||||
|         }, | ||||
| @@ -447,12 +395,6 @@ export default { | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             const selectedSectionId = this.selectedSection && this.selectedSection.id; | ||||
|             const selectedPageId = this.selectedPage && this.selectedPage.id; | ||||
|             if (selectedPageId === pageId && selectedSectionId === sectionId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.sectionsChanged({ sections }); | ||||
|         }, | ||||
|         newEntry(embed = null) { | ||||
| @@ -500,10 +442,10 @@ export default { | ||||
|         async updateDefaultNotebook(notebookStorage) { | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             if (!defaultNotebookObject) { | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { | ||||
|                 this.removeDefaultClass(defaultNotebookObject); | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } | ||||
|  | ||||
|             if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { | ||||
| @@ -572,13 +514,6 @@ export default { | ||||
|  | ||||
|             setDefaultNotebookSection(section); | ||||
|         }, | ||||
|         updateEntry(entry) { | ||||
|             const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage); | ||||
|             const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage); | ||||
|             entries[entryPos] = entry; | ||||
|  | ||||
|             this.updateEntries(entries); | ||||
|         }, | ||||
|         updateEntries(entries) { | ||||
|             const configuration = this.internalDomainObject.configuration; | ||||
|             const notebookEntries = configuration.entries || {}; | ||||
|   | ||||
| @@ -33,10 +33,10 @@ import SnapshotTemplate from './snapshot-template.html'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         PopupMenu | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         embed: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -12,15 +12,11 @@ | ||||
|         <div class="c-ne__content"> | ||||
|             <div :id="entry.id" | ||||
|                  class="c-ne__text" | ||||
|                  tabindex="0" | ||||
|                  :class="{ 'c-ne__input' : !readOnly }" | ||||
|                  :class="{'c-ne__input' : !readOnly }" | ||||
|                  :contenteditable="!readOnly" | ||||
|                  @blur="updateEntryValue($event)" | ||||
|                  @keydown.enter.exact.prevent | ||||
|                  @keyup.enter.exact.prevent="forceBlur($event)" | ||||
|                  v-text="entry.text" | ||||
|             > | ||||
|             </div> | ||||
|                  @blur="updateEntryValue($event, entry.id)" | ||||
|                  @focus="updateCurrentEntryValue($event, entry.id)" | ||||
|             >{{ entry.text }}</div> | ||||
|             <div class="c-snapshots c-ne__embeds"> | ||||
|                 <NotebookEmbed v-for="embed in entry.embeds" | ||||
|                                :key="embed.id" | ||||
| @@ -37,7 +33,6 @@ | ||||
|     > | ||||
|         <button class="c-icon-button c-icon-button--major icon-trash" | ||||
|                 title="Delete this entry" | ||||
|                 tabindex="-1" | ||||
|                 @click="deleteEntry" | ||||
|         > | ||||
|         </button> | ||||
| @@ -62,14 +57,14 @@ | ||||
|  | ||||
| <script> | ||||
| import NotebookEmbed from './NotebookEmbed.vue'; | ||||
| import { createNewEmbed } from '../utils/notebook-entries'; | ||||
| import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries'; | ||||
| import Moment from 'moment'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     components: { | ||||
|         NotebookEmbed | ||||
|     }, | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -108,6 +103,11 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentEntryValue: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         createdOnDate() { | ||||
|             return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD'); | ||||
| @@ -117,20 +117,10 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.updateEntries = this.updateEntries.bind(this); | ||||
|         this.dropOnEntry = this.dropOnEntry.bind(this); | ||||
|     }, | ||||
|     methods: { | ||||
|         addNewEmbed(objectPath) { | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const snapshotMeta = { | ||||
|                 bounds, | ||||
|                 link: null, | ||||
|                 objectPath, | ||||
|                 openmct: this.openmct | ||||
|             }; | ||||
|             const newEmbed = createNewEmbed(snapshotMeta); | ||||
|             this.entry.embeds.push(newEmbed); | ||||
|         }, | ||||
|         cancelEditMode(event) { | ||||
|             const isEditing = this.openmct.editor.isEditing(); | ||||
|             if (isEditing) { | ||||
| @@ -142,23 +132,63 @@ export default { | ||||
|             event.dataTransfer.dropEffect = "copy"; | ||||
|         }, | ||||
|         deleteEntry() { | ||||
|             this.$emit('deleteEntry', this.entry.id); | ||||
|             const self = this; | ||||
|             const entryPosById = self.entryPosById(self.entry.id); | ||||
|             if (entryPosById === -1) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const dialog = this.openmct.overlays.dialog({ | ||||
|                 iconClass: 'alert', | ||||
|                 message: 'This action will permanently delete this entry. Do you wish to continue?', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: "Ok", | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage); | ||||
|                             entries.splice(entryPosById, 1); | ||||
|                             self.updateEntries(entries); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         label: "Cancel", | ||||
|                         callback: () => { | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|         }, | ||||
|         dropOnEntry($event) { | ||||
|             event.stopImmediatePropagation(); | ||||
|  | ||||
|             const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|                 this.snapshotContainer.removeSnapshot(snapshotId); | ||||
|                 this.entry.embeds.push(snapshot); | ||||
|             } else { | ||||
|                 const data = $event.dataTransfer.getData('openmct/domain-object-path'); | ||||
|                 const objectPath = JSON.parse(data); | ||||
|                 this.addNewEmbed(objectPath); | ||||
|                 this.moveSnapshot(snapshotId); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('updateEntry', this.entry); | ||||
|             const data = $event.dataTransfer.getData('openmct/domain-object-path'); | ||||
|             const objectPath = JSON.parse(data); | ||||
|             const entryPos = this.entryPosById(this.entry.id); | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const snapshotMeta = { | ||||
|                 bounds, | ||||
|                 link: null, | ||||
|                 objectPath, | ||||
|                 openmct: this.openmct | ||||
|             }; | ||||
|             const newEmbed = createNewEmbed(snapshotMeta); | ||||
|             const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|             const currentEntryEmbeds = entries[entryPos].embeds; | ||||
|             currentEntryEmbeds.push(newEmbed); | ||||
|             this.updateEntries(entries); | ||||
|         }, | ||||
|         entryPosById(entryId) { | ||||
|             return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage); | ||||
|         }, | ||||
|         findPositionInArray(array, id) { | ||||
|             let position = -1; | ||||
| @@ -173,12 +203,15 @@ export default { | ||||
|  | ||||
|             return position; | ||||
|         }, | ||||
|         forceBlur(event) { | ||||
|             event.target.blur(); | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|         }, | ||||
|         moveSnapshot(snapshotId) { | ||||
|             const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|             this.entry.embeds.push(snapshot); | ||||
|             this.updateEntry(this.entry); | ||||
|             this.snapshotContainer.removeSnapshot(snapshotId); | ||||
|         }, | ||||
|         navigateToPage() { | ||||
|             this.$emit('changeSectionPage', { | ||||
|                 sectionId: this.result.section.id, | ||||
| @@ -194,8 +227,15 @@ export default { | ||||
|         removeEmbed(id) { | ||||
|             const embedPosition = this.findPositionInArray(this.entry.embeds, id); | ||||
|             this.entry.embeds.splice(embedPosition, 1); | ||||
|             this.updateEntry(this.entry); | ||||
|         }, | ||||
|         updateCurrentEntryValue($event) { | ||||
|             if (this.readOnly) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('updateEntry', this.entry); | ||||
|             const target = $event.target; | ||||
|             this.currentEntryValue = target ? target.textContent : ''; | ||||
|         }, | ||||
|         updateEmbed(newEmbed) { | ||||
|             this.entry.embeds.some(e => { | ||||
| @@ -207,14 +247,44 @@ export default { | ||||
|                 return found; | ||||
|             }); | ||||
|  | ||||
|             this.$emit('updateEntry', this.entry); | ||||
|             this.updateEntry(this.entry); | ||||
|         }, | ||||
|         updateEntryValue($event) { | ||||
|             const value = $event.target.innerText; | ||||
|             if (value !== this.entry.text && value.match(/\S/)) { | ||||
|                 this.entry.text = value; | ||||
|                 this.$emit('updateEntry', this.entry); | ||||
|         updateEntry(newEntry) { | ||||
|             const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|             entries.some(entry => { | ||||
|                 const found = (entry.id === newEntry.id); | ||||
|                 if (found) { | ||||
|                     entry = newEntry; | ||||
|                 } | ||||
|  | ||||
|                 return found; | ||||
|             }); | ||||
|  | ||||
|             this.updateEntries(entries); | ||||
|         }, | ||||
|         updateEntryValue($event, entryId) { | ||||
|             if (!this.domainObject || !this.selectedSection || !this.selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const target = $event.target; | ||||
|             if (!target) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const entryPos = this.entryPosById(entryId); | ||||
|             const value = target.textContent.trim(); | ||||
|             if (this.currentEntryValue !== value) { | ||||
|                 target.textContent = value; | ||||
|  | ||||
|                 const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|                 entries[entryPos].text = value; | ||||
|  | ||||
|                 this.updateEntries(entries); | ||||
|             } | ||||
|         }, | ||||
|         updateEntries(entries) { | ||||
|             this.$emit('updateEntries', entries); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -44,46 +44,38 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             notebookSnapshot: undefined, | ||||
|             notebookSnapshot: null, | ||||
|             notebookTypes: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         validateNotebookStorageObject(); | ||||
|         this.getDefaultNotebookObject(); | ||||
|  | ||||
|         this.notebookSnapshot = new Snapshot(this.openmct); | ||||
|         this.setDefaultNotebookStatus(); | ||||
|     }, | ||||
|     methods: { | ||||
|         async getDefaultNotebookObject() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const defaultNotebookObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); | ||||
|  | ||||
|             return defaultNotebookObject; | ||||
|         }, | ||||
|         async showMenu(event) { | ||||
|         showMenu(event) { | ||||
|             const notebookTypes = []; | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const elementBoundingClientRect = this.$el.getBoundingClientRect(); | ||||
|             const x = elementBoundingClientRect.x; | ||||
|             const y = elementBoundingClientRect.y + elementBoundingClientRect.height; | ||||
|  | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             if (defaultNotebookObject) { | ||||
|                 const name = defaultNotebookObject.name; | ||||
|             if (defaultNotebook) { | ||||
|                 const domainObject = defaultNotebook.domainObject; | ||||
|  | ||||
|                 const defaultNotebook = getDefaultNotebook(); | ||||
|                 const sectionName = defaultNotebook.section.name; | ||||
|                 const pageName = defaultNotebook.page.name; | ||||
|                 const defaultPath = `${name} - ${sectionName} - ${pageName}`; | ||||
|                 if (domainObject.location) { | ||||
|                     const defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|  | ||||
|                 notebookTypes.push({ | ||||
|                     cssClass: 'icon-notebook', | ||||
|                     name: `Save to Notebook ${defaultPath}`, | ||||
|                     callBack: () => { | ||||
|                         return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                     } | ||||
|                 }); | ||||
|                     notebookTypes.push({ | ||||
|                         cssClass: 'icon-notebook', | ||||
|                         name: `Save to Notebook ${defaultPath}`, | ||||
|                         callBack: () => { | ||||
|                             return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             notebookTypes.push({ | ||||
|   | ||||
| @@ -56,11 +56,11 @@ import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container'; | ||||
| import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     components: { | ||||
|         NotebookEmbed, | ||||
|         PopupMenu | ||||
|     }, | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     props: { | ||||
|         toggleSnapshot: { | ||||
|             type: Function, | ||||
|   | ||||
| @@ -69,14 +69,14 @@ export default { | ||||
|             const divElement = document.querySelector('.l-shell__drawer div'); | ||||
|  | ||||
|             this.component = new Vue({ | ||||
|                 el: divElement, | ||||
|                 components: { | ||||
|                     SnapshotContainerComponent | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct, | ||||
|                     snapshotContainer | ||||
|                 }, | ||||
|                 el: divElement, | ||||
|                 components: { | ||||
|                     SnapshotContainerComponent | ||||
|                 }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         toggleSnapshot | ||||
|   | ||||
| @@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import Page from './PageComponent.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         Page | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         defaultPageId: { | ||||
|             type: String, | ||||
|   | ||||
| @@ -18,10 +18,10 @@ import PopupMenu from './PopupMenu.vue'; | ||||
| import RemoveDialog from '../utils/removeDialog'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         PopupMenu | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         defaultPageId: { | ||||
|             type: String, | ||||
|   | ||||
| @@ -21,10 +21,10 @@ | ||||
| import NotebookEntry from './NotebookEntry.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     components: { | ||||
|         NotebookEntry | ||||
|     }, | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import sectionComponent from './SectionComponent.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         sectionComponent | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         defaultSectionId: { | ||||
|             type: String, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user