Compare commits
	
		
			104 Commits
		
	
	
		
			version-1.
			...
			openmct-st
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 46ea383b51 | ||
|   | 7df7d57bc2 | ||
|   | 5f03dc45ee | ||
|   | 14ac758760 | ||
|   | eb709a60cb | ||
|   | eba1a48a44 | ||
|   | 4a0654dbcb | ||
|   | 9b6d339d69 | ||
|   | f90afb9277 | ||
|   | 018dfb1e28 | ||
|   | 5646a252f7 | ||
|   | c72a02aaa3 | ||
|   | 0e6ce7f58b | ||
|   | 8cd6a4c6a3 | ||
|   | 02fc162197 | ||
|   | 84d21a3695 | ||
|   | 1a6369c2b9 | ||
|   | 463c44679d | ||
|   | c1f3ea4e61 | ||
|   | bf3fd66942 | ||
|   | 8414ded1ec | ||
|   | 142b767470 | ||
|   | 184b716b53 | ||
|   | 646c871c76 | ||
|   | e53399495b | ||
|   | d27f73579b | ||
|   | ba401b3341 | ||
|   | 5ef02ec4a2 | ||
|   | d788031019 | ||
|   | 1ae8199e89 | ||
|   | 2deb4e8474 | ||
|   | 7f10681424 | ||
|   | c756adad6f | ||
|   | f3d593bc1e | ||
|   | b637307de6 | ||
|   | b6e0208e71 | ||
|   | 631876cab3 | ||
|   | a192d46c2b | ||
|   | 6923f17645 | ||
|   | d870874649 | ||
|   | 711a7a2eb5 | ||
|   | 87a45de05b | ||
|   | ab76451360 | ||
|   | c105a08cfe | ||
|   | b87375a809 | ||
|   | 9fed056d22 | ||
|   | 251bf21933 | ||
|   | a180bf7c02 | ||
|   | ed8a54f0f9 | ||
|   | a91179091f | ||
|   | 5f7e34ce6c | ||
|   | ff3c2da0f9 | ||
|   | db33f0538a | ||
|   | 28d5821120 | ||
|   | f5ee457274 | ||
|   | 9d2770e4d2 | ||
|   | 257a8e2e2d | ||
|   | 8b25009816 | ||
|   | 074fe4481a | ||
|   | fbd928b842 | ||
|   | 110947db09 | ||
|   | baa8078d23 | ||
|   | ef91e92fbc | ||
|   | d201cac4ac | ||
|   | dcb3ccfec7 | ||
|   | 78522cd4f1 | ||
|   | ca232d45cc | ||
|   | df495c841a | ||
|   | 92a37ef36b | ||
|   | fd731ca430 | ||
|   | 263b1cd3d5 | ||
|   | 978fc8b5a3 | ||
|   | 698ccc5a35 | ||
|   | e5aa5b5a5f | ||
|   | b942988ef8 | ||
|   | 1eec20f2ea | ||
|   | 767a2048eb | ||
|   | e65cf1661c | ||
|   | 0eae48646c | ||
|   | 0ba8a275d2 | ||
|   | d8d32cc3ac | ||
|   | a800848fe1 | ||
|   | 6881d98ba6 | ||
|   | 48d077cd2e | ||
|   | 030dd93c91 | ||
|   | 03bf6fc0a3 | ||
|   | ef0a2ed5d2 | ||
|   | a40aa84752 | ||
|   | d3b69dda82 | ||
|   | d3126ebf5c | ||
|   | 4479cbc7a2 | ||
|   | f8ff44dac0 | ||
|   | 8f4280d15b | ||
|   | 6daa27ff31 | ||
|   | 43f6c3f85d | ||
|   | 1a7c76cf3e | ||
|   | cee9cd7bd1 | ||
|   | c42df20281 | ||
|   | b4149bd2b3 | ||
|   | f436ac9ba0 | ||
|   | 8493b481dd | ||
|   | 28723b59b7 | ||
|   | 9fa7de0b77 | ||
|   | 54bfc84ada | 
| @@ -27,7 +27,7 @@ define([ | ||||
| ) { | ||||
|     function ImageryPlugin() { | ||||
|  | ||||
|         var IMAGE_SAMPLES = [ | ||||
|         const IMAGE_SAMPLES = [ | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg", | ||||
| @@ -47,13 +47,14 @@ define([ | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg" | ||||
|         ]; | ||||
|         const IMAGE_DELAY = 20000; | ||||
|  | ||||
|         function pointForTimestamp(timestamp, name) { | ||||
|             return { | ||||
|                 name: name, | ||||
|                 utc: Math.floor(timestamp / 5000) * 5000, | ||||
|                 local: Math.floor(timestamp / 5000) * 5000, | ||||
|                 url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length] | ||||
|                 utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length] | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -64,7 +65,7 @@ define([ | ||||
|             subscribe: function (domainObject, callback) { | ||||
|                 var interval = setInterval(function () { | ||||
|                     callback(pointForTimestamp(Date.now(), domainObject.name)); | ||||
|                 }, 5000); | ||||
|                 }, IMAGE_DELAY); | ||||
|  | ||||
|                 return function () { | ||||
|                     clearInterval(interval); | ||||
| @@ -81,9 +82,9 @@ define([ | ||||
|                 var start = options.start; | ||||
|                 var end = Math.min(options.end, Date.now()); | ||||
|                 var data = []; | ||||
|                 while (start <= end && data.length < 5000) { | ||||
|                 while (start <= end && data.length < IMAGE_DELAY) { | ||||
|                     data.push(pointForTimestamp(start, domainObject.name)); | ||||
|                     start += 5000; | ||||
|                     start += IMAGE_DELAY; | ||||
|                 } | ||||
|  | ||||
|                 return Promise.resolve(data); | ||||
|   | ||||
							
								
								
									
										56
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								index.html
									
									
									
									
									
								
							| @@ -35,7 +35,13 @@ | ||||
|     </body> | ||||
|     <script> | ||||
|         const THIRTY_SECONDS = 30 * 1000; | ||||
|         const THIRTY_MINUTES = THIRTY_SECONDS * 60; | ||||
|         const ONE_MINUTE = THIRTY_SECONDS * 2; | ||||
|         const FIVE_MINUTES = ONE_MINUTE * 5; | ||||
|         const FIFTEEN_MINUTES = FIVE_MINUTES * 3; | ||||
|         const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; | ||||
|         const ONE_HOUR = THIRTY_MINUTES * 2; | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|         [ | ||||
|             'example/eventGenerator' | ||||
| @@ -48,6 +54,7 @@ | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
| @@ -72,21 +79,21 @@ | ||||
|                         { | ||||
|                             label: 'Last Day', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60 * 24, | ||||
|                                 start: () => Date.now() - ONE_DAY, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last 2 hours', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60 * 2, | ||||
|                                 start: () => Date.now() - TWO_HOURS, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last hour', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60, | ||||
|                                 start: () => Date.now() - ONE_HOUR, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         } | ||||
| @@ -95,7 +102,7 @@ | ||||
|                     records: 10, | ||||
|                     // maximum duration between start and end bounds | ||||
|                     // for utc-based time systems this is in milliseconds | ||||
|                     limit: 1000 * 60 * 60 * 24 | ||||
|                     limit: ONE_DAY | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Realtime", | ||||
| @@ -104,7 +111,44 @@ | ||||
|                     clockOffsets: { | ||||
|                         start: - THIRTY_MINUTES, | ||||
|                         end: THIRTY_SECONDS | ||||
|                     } | ||||
|                     }, | ||||
|                     presets: [ | ||||
|                         { | ||||
|                             label: '1 Hour', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_HOUR, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '30 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - THIRTY_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '15 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIFTEEN_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '5 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIVE_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '1 Minute', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_MINUTE, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         })); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.3.1-SNAPSHOT", | ||||
|   "version": "1.3.3-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|   | ||||
| @@ -143,8 +143,8 @@ define([ | ||||
|                             "$window" | ||||
|                         ], | ||||
|                         "group": "windowing", | ||||
|                         "cssClass": "icon-new-window", | ||||
|                         "priority": "preferred" | ||||
|                         "priority": 10, | ||||
|                         "cssClass": "icon-new-window" | ||||
|                     } | ||||
|                 ], | ||||
|                 "runs": [ | ||||
|   | ||||
| @@ -139,7 +139,9 @@ define([ | ||||
|                         ], | ||||
|                         "description": "Edit", | ||||
|                         "category": "view-control", | ||||
|                         "cssClass": "major icon-pencil" | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "group": "action", | ||||
|                         "priority": 10 | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "properties", | ||||
| @@ -150,6 +152,8 @@ define([ | ||||
|                         "implementation": PropertiesAction, | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "name": "Edit Properties...", | ||||
|                         "group": "action", | ||||
|                         "priority": 10, | ||||
|                         "description": "Edit properties of this object.", | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|   | ||||
| @@ -20,12 +20,12 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="c-object-label" | ||||
|      ng-class="{ 'is-missing': model.status === 'missing' }" | ||||
|      ng-class="{ 'is-status--missing': model.status === 'missing' }" | ||||
| > | ||||
|     <div class="c-object-label__type-icon {{type.getCssClass()}}" | ||||
|          ng-class="{ 'l-icon-link':location.isLink() }" | ||||
|     > | ||||
|         <span class="is-missing__indicator" title="This item is missing"></span> | ||||
|         <span class="is-status__indicator" title="This item is missing or suspect"></span> | ||||
|     </div> | ||||
|     <div class='c-object-label__name'>{{model.name}}</div> | ||||
| </div> | ||||
|   | ||||
| @@ -114,7 +114,12 @@ define(["objectUtils"], | ||||
|             var self = this, | ||||
|                 domainObject = this.domainObject; | ||||
|  | ||||
|             let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId()); | ||||
|             const identifier = { | ||||
|                 namespace: this.getSpace(), | ||||
|                 key: this.getKey() | ||||
|             }; | ||||
|  | ||||
|             let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier); | ||||
|  | ||||
|             return this.openmct.objects | ||||
|                 .save(newStyleObject) | ||||
| @@ -146,6 +151,7 @@ define(["objectUtils"], | ||||
|                     return domainObject.useCapability("mutation", function () { | ||||
|                         return model; | ||||
|                     }, modified); | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -99,8 +99,8 @@ define( | ||||
|  | ||||
|                 mockNewStyleDomainObject = Object.assign({}, model); | ||||
|                 mockNewStyleDomainObject.identifier = { | ||||
|                     namespace: "", | ||||
|                     key: id | ||||
|                     namespace: SPACE, | ||||
|                     key: key | ||||
|                 }; | ||||
|  | ||||
|                 // Simulate mutation capability | ||||
|   | ||||
| @@ -66,6 +66,8 @@ define([ | ||||
|                         "description": "Move object to another location.", | ||||
|                         "cssClass": "icon-move", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 9, | ||||
|                         "implementation": MoveAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
| @@ -79,6 +81,8 @@ define([ | ||||
|                         "description": "Duplicate object to another location.", | ||||
|                         "cssClass": "icon-duplicate", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 8, | ||||
|                         "implementation": CopyAction, | ||||
|                         "depends": [ | ||||
|                             "$log", | ||||
| @@ -95,6 +99,8 @@ define([ | ||||
|                         "description": "Create Link to object in another location.", | ||||
|                         "cssClass": "icon-link", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 7, | ||||
|                         "implementation": LinkAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
|   | ||||
| @@ -29,7 +29,6 @@ define(["zepto"], function ($) { | ||||
|      * @memberof platform/forms | ||||
|      */ | ||||
|     function FileInputService() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -38,7 +37,7 @@ define(["zepto"], function ($) { | ||||
|      * | ||||
|      * @returns {Promise} promise for an object containing file meta-data | ||||
|      */ | ||||
|     FileInputService.prototype.getInput = function () { | ||||
|     FileInputService.prototype.getInput = function (fileType) { | ||||
|         var input = this.newInput(); | ||||
|         var read = this.readFile; | ||||
|         var fileInfo = {}; | ||||
| @@ -51,6 +50,10 @@ define(["zepto"], function ($) { | ||||
|                 file = this.files[0]; | ||||
|                 input.remove(); | ||||
|                 if (file) { | ||||
|                     if (fileType && (!file.type || (file.type !== fileType))) { | ||||
|                         reject("Incompatible file type"); | ||||
|                     } | ||||
|  | ||||
|                     read(file) | ||||
|                         .then(function (contents) { | ||||
|                             fileInfo.name = file.name; | ||||
|   | ||||
| @@ -40,7 +40,7 @@ define( | ||||
|                 } | ||||
|  | ||||
|                 function handleClick() { | ||||
|                     fileInputService.getInput().then(function (result) { | ||||
|                     fileInputService.getInput(scope.structure.type).then(function (result) { | ||||
|                         setText(result.name); | ||||
|                         scope.ngModel[scope.field] = result; | ||||
|                         control.$setValidity("file-input", true); | ||||
|   | ||||
| @@ -47,6 +47,8 @@ define([ | ||||
|                             "implementation": ExportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-export", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "openmct", | ||||
|                                 "exportService", | ||||
| @@ -61,6 +63,8 @@ define([ | ||||
|                             "implementation": ImportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-import", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "exportService", | ||||
|                                 "identifierService", | ||||
|   | ||||
| @@ -242,7 +242,11 @@ define([ | ||||
|  | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|  | ||||
|         this.contextMenu = new api.ContextMenuRegistry(); | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|  | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|         this.router = new ApplicationRouter(); | ||||
|  | ||||
| @@ -271,6 +275,7 @@ define([ | ||||
|         this.install(this.plugins.URLTimeSettingsSynchronizer()); | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
| @@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) { | ||||
|  | ||||
|     legacyActions.filter(contextualCategoryOnly) | ||||
|         .map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction)) | ||||
|         .forEach(openmct.contextMenu.registerAction); | ||||
|         .forEach(openmct.actions.register); | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,8 @@ export default class LegacyContextMenuAction { | ||||
|         this.description = LegacyAction.definition.description; | ||||
|         this.cssClass = LegacyAction.definition.cssClass; | ||||
|         this.LegacyAction = LegacyAction; | ||||
|         this.group = LegacyAction.definition.group; | ||||
|         this.priority = LegacyAction.definition.priority; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath) { | ||||
|   | ||||
| @@ -128,7 +128,7 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     ObjectServiceProvider.prototype.get = function (key) { | ||||
|         const keyString = utils.makeKeyString(key); | ||||
|         let keyString = utils.makeKeyString(key); | ||||
|  | ||||
|         return this.objectService.getObjects([keyString]) | ||||
|             .then(function (results) { | ||||
|   | ||||
| @@ -20,6 +20,8 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import utils from 'objectUtils'; | ||||
|  | ||||
| export default class LegacyPersistenceAdapter { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
| @@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter { | ||||
|         return Promise.resolve(Object.keys(this.openmct.objects.providers)); | ||||
|     } | ||||
|  | ||||
|     updateObject(legacyDomainObject) { | ||||
|         return this.openmct.objects.save(legacyDomainObject.useCapability('adapter')); | ||||
|     createObject(space, key, legacyDomainObject) { | ||||
|         let object = utils.toNewFormat(legacyDomainObject, { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }); | ||||
|  | ||||
|         return this.openmct.objects.save(object); | ||||
|     } | ||||
|  | ||||
|     deleteObject(space, key) { | ||||
|         const identifier = { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }; | ||||
|  | ||||
|         return this.openmct.objects.delete(identifier); | ||||
|     } | ||||
|  | ||||
|     updateObject(space, key, legacyDomainObject) { | ||||
|         let object = utils.toNewFormat(legacyDomainObject, { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }); | ||||
|  | ||||
|         return this.openmct.objects.save(object); | ||||
|     } | ||||
|  | ||||
|     readObject(space, key) { | ||||
|   | ||||
							
								
								
									
										178
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from 'EventEmitter'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionCollection extends EventEmitter { | ||||
|     constructor(applicableActions, objectPath, view, openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.applicableActions = applicableActions; | ||||
|         this.openmct = openmct; | ||||
|         this.objectPath = objectPath; | ||||
|         this.view = view; | ||||
|         this.objectUnsubscribes = []; | ||||
|  | ||||
|         let debounceOptions = { | ||||
|             leading: false, | ||||
|             trailing: true | ||||
|         }; | ||||
|  | ||||
|         this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions); | ||||
|         this._update = _.debounce(this._update.bind(this), 150, debounceOptions); | ||||
|  | ||||
|         this._observeObjectPath(); | ||||
|         this._initializeActions(); | ||||
|  | ||||
|         this.openmct.editor.on('isEditing', this._updateActions); | ||||
|     } | ||||
|  | ||||
|     disable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     enable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     hide(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     show(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|             unsubscribe(); | ||||
|         }); | ||||
|  | ||||
|         this.openmct.editor.off('isEditing', this._updateActions); | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let visibleActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (!action.isHidden) { | ||||
|                 visibleActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return visibleActions; | ||||
|     } | ||||
|  | ||||
|     getStatusBarActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let statusBarActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (action.showInStatusBar && !action.isDisabled && !action.isHidden) { | ||||
|                 statusBarActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return statusBarActions; | ||||
|     } | ||||
|  | ||||
|     _update() { | ||||
|         this.emit('update', this.applicableActions); | ||||
|     } | ||||
|  | ||||
|     _observeObjectPath() { | ||||
|         let actionCollection = this; | ||||
|  | ||||
|         function updateObject(oldObject, newObject) { | ||||
|             Object.assign(oldObject, newObject); | ||||
|  | ||||
|             actionCollection._updateActions(); | ||||
|         } | ||||
|  | ||||
|         this.objectPath.forEach(object => { | ||||
|             if (object) { | ||||
|                 let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object)); | ||||
|  | ||||
|                 this.objectUnsubscribes.push(unsubscribe); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _initializeActions() { | ||||
|         Object.keys(this.applicableActions).forEach(key => { | ||||
|             this.applicableActions[key].callBack = () => { | ||||
|                 return this.applicableActions[key].invoke(this.objectPath, this.view); | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _updateActions() { | ||||
|         let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); | ||||
|  | ||||
|         this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); | ||||
|         this._initializeActions(); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     _mergeOldAndNewActions(oldActions, newActions) { | ||||
|         let mergedActions = {}; | ||||
|         Object.keys(newActions).forEach(key => { | ||||
|             if (oldActions[key]) { | ||||
|                 mergedActions[key] = oldActions[key]; | ||||
|             } else { | ||||
|                 mergedActions[key] = newActions[key]; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return mergedActions; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionCollection; | ||||
							
								
								
									
										145
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from 'EventEmitter'; | ||||
| import ActionCollection from './ActionCollection'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionsAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._allActions = {}; | ||||
|         this._actionCollections = new WeakMap(); | ||||
|         this._openmct = openmct; | ||||
|  | ||||
|         this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; | ||||
|  | ||||
|         this.register = this.register.bind(this); | ||||
|         this.get = this.get.bind(this); | ||||
|         this._applicableActions = this._applicableActions.bind(this); | ||||
|         this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); | ||||
|     } | ||||
|  | ||||
|     register(actionDefinition) { | ||||
|         this._allActions[actionDefinition.key] = actionDefinition; | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         let viewContext = view && view.getViewContext && view.getViewContext() || {}; | ||||
|  | ||||
|         if (view && !viewContext.skipCache) { | ||||
|             let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
|             if (cachedActionCollection) { | ||||
|                 return cachedActionCollection; | ||||
|             } else { | ||||
|                 let applicableActions = this._applicableActions(objectPath, view); | ||||
|                 let actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct); | ||||
|  | ||||
|                 this._actionCollections.set(view, actionCollection); | ||||
|                 actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|                 return actionCollection; | ||||
|             } | ||||
|         } else { | ||||
|             let applicableActions = this._applicableActions(objectPath, view); | ||||
|  | ||||
|             Object.keys(applicableActions).forEach(key => { | ||||
|                 let action = applicableActions[key]; | ||||
|  | ||||
|                 action.callBack = () => { | ||||
|                     return action.invoke(objectPath, view); | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return applicableActions; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateGroupOrder(groupArray) { | ||||
|         this._groupOrder = groupArray; | ||||
|     } | ||||
|  | ||||
|     _updateCachedActionCollections(key) { | ||||
|         if (this._actionCollections.has(key)) { | ||||
|             let actionCollection = this._actionCollections.get(key); | ||||
|             actionCollection.off('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|             this._actionCollections.delete(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _applicableActions(objectPath, view) { | ||||
|         let actionsObject = {}; | ||||
|  | ||||
|         let keys = Object.keys(this._allActions).filter(key => { | ||||
|             let actionDefinition = this._allActions[key]; | ||||
|  | ||||
|             if (actionDefinition.appliesTo === undefined) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return actionDefinition.appliesTo(objectPath, view); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         keys.forEach(key => { | ||||
|             let action = _.clone(this._allActions[key]); | ||||
|  | ||||
|             actionsObject[key] = action; | ||||
|         }); | ||||
|  | ||||
|         return actionsObject; | ||||
|     } | ||||
|  | ||||
|     _groupAndSortActions(actionsArray) { | ||||
|         if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { | ||||
|             actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); | ||||
|         } | ||||
|  | ||||
|         let actionsObject = {}; | ||||
|         let groupedSortedActionsArray = []; | ||||
|  | ||||
|         function sortDescending(a, b) { | ||||
|             return b.priority - a.priority; | ||||
|         } | ||||
|  | ||||
|         actionsArray.forEach(action => { | ||||
|             if (actionsObject[action.group] === undefined) { | ||||
|                 actionsObject[action.group] = [action]; | ||||
|             } else { | ||||
|                 actionsObject[action.group].push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this._groupOrder.forEach(group => { | ||||
|             let groupArray = actionsObject[group]; | ||||
|  | ||||
|             if (groupArray) { | ||||
|                 groupedSortedActionsArray.push(groupArray.sort(sortDescending)); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return groupedSortedActionsArray; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionsAPI; | ||||
| @@ -28,9 +28,10 @@ define([ | ||||
|     './telemetry/TelemetryAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './notifications/NotificationAPI', | ||||
|     './contextMenu/ContextMenuAPI', | ||||
|     './Editor' | ||||
|  | ||||
|     './Editor', | ||||
|     './menu/MenuAPI', | ||||
|     './actions/ActionsAPI', | ||||
|     './status/StatusAPI' | ||||
| ], function ( | ||||
|     TimeAPI, | ||||
|     ObjectAPI, | ||||
| @@ -39,8 +40,10 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     IndicatorAPI, | ||||
|     NotificationAPI, | ||||
|     ContextMenuAPI, | ||||
|     EditorAPI | ||||
|     EditorAPI, | ||||
|     MenuAPI, | ||||
|     ActionsAPI, | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
| @@ -51,6 +54,8 @@ define([ | ||||
|         IndicatorAPI: IndicatorAPI, | ||||
|         NotificationAPI: NotificationAPI.default, | ||||
|         EditorAPI: EditorAPI, | ||||
|         ContextMenuRegistry: ContextMenuAPI.default | ||||
|         MenuAPI: MenuAPI.default, | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         StatusAPI: StatusAPI.default | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.invoke(objectPath)" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions', 'objectPath'] | ||||
| }; | ||||
| </script> | ||||
| @@ -1,159 +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 ContextMenuComponent from './ContextMenu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| /** | ||||
|  * The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface ContextMenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
| class ContextMenuAPI { | ||||
|     constructor() { | ||||
|         this._allActions = []; | ||||
|         this._activeContextMenu = undefined; | ||||
|  | ||||
|         this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this); | ||||
|         this.registerAction = this.registerAction.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when | ||||
|      * selected. Applicabilioty can be restricted by specification of an `appliesTo` function. | ||||
|      * | ||||
|      * @interface ContextMenuAction | ||||
|      * @memberof module:openmct | ||||
|      * @property {string} name the human-readable name of this view | ||||
|      * @property {string} description a longer-form description (typically | ||||
|      *           a single sentence or short paragraph) of this kind of view | ||||
|      * @property {string} cssClass the CSS class to apply to labels for this | ||||
|      *           view (to add icons, for instance) | ||||
|      * @property {string} key unique key to identify the context menu action | ||||
|      *           (used in custom context menu eg table rows, to identify which actions to include) | ||||
|      * @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item) | ||||
|      */ | ||||
|     /** | ||||
|      * @method appliesTo | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on. | ||||
|      * @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false. | ||||
|      */ | ||||
|     /** | ||||
|      * Code to be executed when the action is selected from a context menu | ||||
|      * @method invoke | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object to invoke the action on. | ||||
|      */ | ||||
|     /** | ||||
|      * @param {ContextMenuAction} actionDefinition | ||||
|      */ | ||||
|     registerAction(actionDefinition) { | ||||
|         this._allActions.push(actionDefinition); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) { | ||||
|  | ||||
|         let applicableActions = this._allActions.filter((action) => { | ||||
|  | ||||
|             if (actionsToBeIncluded) { | ||||
|                 if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key); | ||||
|             } else { | ||||
|                 if (action.appliesTo === undefined) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath) && !action.hideInDefaultMenu; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (this._activeContextMenu) { | ||||
|             this._hideActiveContextMenu(); | ||||
|         } | ||||
|  | ||||
|         this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions); | ||||
|         this._activeContextMenu.$mount(); | ||||
|         document.body.appendChild(this._activeContextMenu.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$el.style.left = `${position.x}px`; | ||||
|         this._activeContextMenu.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this._hideActiveContextMenu); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _hideActiveContextMenu() { | ||||
|         document.removeEventListener('click', this._hideActiveContextMenu); | ||||
|         document.body.removeChild(this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$destroy(); | ||||
|         this._activeContextMenu = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _createContextMenuForObject(objectPath, actions) { | ||||
|         return new Vue({ | ||||
|             components: { | ||||
|                 ContextMenu: ContextMenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 actions: actions, | ||||
|                 objectPath: objectPath | ||||
|             }, | ||||
|             template: '<ContextMenu></ContextMenu>' | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| export default ContextMenuAPI; | ||||
							
								
								
									
										67
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 Menu from './menu.js'; | ||||
|  | ||||
| /** | ||||
|  * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface MenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
| class MenuAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.showMenu = this.showMenu.bind(this); | ||||
|         this._clearMenuComponent = this._clearMenuComponent.bind(this); | ||||
|         this._showObjectMenu = this._showObjectMenu.bind(this); | ||||
|     } | ||||
|  | ||||
|     showMenu(x, y, actions) { | ||||
|         if (this.menuComponent) { | ||||
|             this.menuComponent.dismiss(); | ||||
|         } | ||||
|  | ||||
|         let options = { | ||||
|             x, | ||||
|             y, | ||||
|             actions | ||||
|         }; | ||||
|  | ||||
|         this.menuComponent = new Menu(options); | ||||
|         this.menuComponent.once('destroy', this._clearMenuComponent); | ||||
|     } | ||||
|  | ||||
|     _clearMenuComponent() { | ||||
|         this.menuComponent = undefined; | ||||
|         delete this.menuComponent; | ||||
|     } | ||||
|  | ||||
|     _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { | ||||
|         let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded); | ||||
|  | ||||
|         this.showMenu(x, y, applicableActions); | ||||
|     } | ||||
| } | ||||
| export default MenuAPI; | ||||
							
								
								
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul v-if="actions.length && actions[0].length"> | ||||
|         <template | ||||
|             v-for="(actionGroups, index) in actions" | ||||
|         > | ||||
|             <li | ||||
|                 v-for="action in actionGroups" | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
|             </li> | ||||
|             <div | ||||
|                 v-if="index !== actions.length - 1" | ||||
|                 :key="index" | ||||
|                 class="c-menu__section-separator" | ||||
|             > | ||||
|             </div> | ||||
|             <li | ||||
|                 v-if="actionGroups.length === 0" | ||||
|                 :key="index" | ||||
|             > | ||||
|                 No actions defined. | ||||
|             </li> | ||||
|         </template> | ||||
|     </ul> | ||||
|  | ||||
|     <ul v-else> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions'] | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from 'EventEmitter'; | ||||
| import MenuComponent from './components/Menu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| class Menu extends EventEmitter { | ||||
|     constructor(options) { | ||||
|         super(); | ||||
|  | ||||
|         this.options = options; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 actions: options.actions | ||||
|             }, | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
|         if (options.onDestroy) { | ||||
|             this.once('destroy', options.onDestroy); | ||||
|         } | ||||
|  | ||||
|         this.dismiss = this.dismiss.bind(this); | ||||
|         this.show = this.show.bind(this); | ||||
|  | ||||
|         this.show(); | ||||
|     } | ||||
|  | ||||
|     dismiss() { | ||||
|         this.emit('destroy'); | ||||
|         document.body.removeChild(this.component.$el); | ||||
|         document.removeEventListener('click', this.dismiss); | ||||
|         this.component.$destroy(); | ||||
|     } | ||||
|  | ||||
|     show() { | ||||
|         this.component.$mount(); | ||||
|         document.body.appendChild(this.component.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el); | ||||
|  | ||||
|         this.component.$el.style.left = `${position.x}px`; | ||||
|         this.component.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this.dismiss); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default Menu; | ||||
| @@ -47,6 +47,7 @@ define([ | ||||
|         this.providers = {}; | ||||
|         this.rootRegistry = new RootRegistry(); | ||||
|         this.rootProvider = new RootObjectProvider.default(this.rootRegistry); | ||||
|         this.cache = {}; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -154,6 +155,11 @@ define([ | ||||
|      *          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]; | ||||
|         } | ||||
|  | ||||
|         identifier = utils.parseKeyString(identifier); | ||||
|         const provider = this.getProvider(identifier); | ||||
|  | ||||
| @@ -165,7 +171,15 @@ define([ | ||||
|             throw new Error('Provider does not support get!'); | ||||
|         } | ||||
|  | ||||
|         return provider.get(identifier); | ||||
|         let objectPromise = provider.get(identifier); | ||||
|  | ||||
|         this.cache[keystring] = objectPromise; | ||||
|  | ||||
|         return objectPromise.then(result => { | ||||
|             delete this.cache[keystring]; | ||||
|  | ||||
|             return result; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     ObjectAPI.prototype.delete = function () { | ||||
|   | ||||
| @@ -59,4 +59,25 @@ describe("The Object API", () => { | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The get function", () => { | ||||
|         describe("when a provider is available", () => { | ||||
|             let mockProvider; | ||||
|             beforeEach(() => { | ||||
|                 mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                     "get" | ||||
|                 ]); | ||||
|                 mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); | ||||
|                 objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|             }); | ||||
|  | ||||
|             it("Caches multiple requests for the same object", () => { | ||||
|                 expect(mockProvider.get.calls.count()).toBe(0); | ||||
|                 objectAPI.get(mockDomainObject.identifier); | ||||
|                 expect(mockProvider.get.calls.count()).toBe(1); | ||||
|                 objectAPI.get(mockDomainObject.identifier); | ||||
|                 expect(mockProvider.get.calls.count()).toBe(1); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class OverlayAPI { | ||||
|                 this.dismissLastOverlay(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -127,6 +128,7 @@ class OverlayAPI { | ||||
|  | ||||
|         return progressDialog; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default OverlayAPI; | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| export default class StatusAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._statusCache = {}; | ||||
|  | ||||
|         this.get = this.get.bind(this); | ||||
|         this.set = this.set.bind(this); | ||||
|         this.observe = this.observe.bind(this); | ||||
|     } | ||||
|  | ||||
|     get(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         return this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     set(identifier, value) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = value; | ||||
|         this.emit(keyString, value); | ||||
|     } | ||||
|  | ||||
|     delete(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = undefined; | ||||
|         this.emit(keyString, undefined); | ||||
|         delete this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     observe(identifier, callback) { | ||||
|         let key = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this.on(key, callback); | ||||
|  | ||||
|         return () => { | ||||
|             this.off(key, callback); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import StatusAPI from './StatusAPI.js'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe("The Status API", () => { | ||||
|     let statusAPI; | ||||
|     let openmct; | ||||
|     let identifier; | ||||
|     let status; | ||||
|     let status2; | ||||
|     let callback; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         statusAPI = new StatusAPI(openmct); | ||||
|         identifier = { | ||||
|             namespace: "test-namespace", | ||||
|             key: "test-key" | ||||
|         }; | ||||
|         status = "test-status"; | ||||
|         status2 = 'test-status-deux'; | ||||
|         callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("set function", () => { | ||||
|         it("sets status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("get function", () => { | ||||
|         it("returns status for identifier", () => { | ||||
|             statusAPI.set(identifier, status2); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("delete function", () => { | ||||
|         it("deletes status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|             resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toBeUndefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("observe function", () => { | ||||
|  | ||||
|         it("allows callbacks to be attached to status set and delete events", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(undefined); | ||||
|             unsubscribe(); | ||||
|         }); | ||||
|  | ||||
|         it("returns a unsubscribe function", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             unsubscribe(); | ||||
|  | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledTimes(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -176,7 +176,10 @@ export default { | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|             let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true}); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|         }, | ||||
|         resetValues() { | ||||
|             this.value = '---'; | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| export default class ClearDataAction { | ||||
|     constructor(openmct, appliesToObjects) { | ||||
|         this.name = 'Clear Data for Object'; | ||||
|         this.key = 'clear-data-action'; | ||||
|         this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; | ||||
|         this.cssClass = 'icon-clear-data'; | ||||
|  | ||||
|   | ||||
| @@ -53,7 +53,7 @@ define([ | ||||
|                 openmct.indicators.add(indicator); | ||||
|             } | ||||
|  | ||||
|             openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|             openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js'; | ||||
| describe('When the Clear Data Plugin is installed,', function () { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         contextMenu: mockContextMenuProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         } | ||||
| @@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () { | ||||
|     it('Clear Data context menu action is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockContextMenuProvider.registerAction).toHaveBeenCalled(); | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', function () { | ||||
|   | ||||
| @@ -219,6 +219,18 @@ export default { | ||||
|         isItemType(type, item) { | ||||
|             return item && (item.type === type); | ||||
|         }, | ||||
|         canPersistObject(item) { | ||||
|             // for now the only way to tell if an object can be persisted is if it is creatable. | ||||
|             let creatable = false; | ||||
|             if (item) { | ||||
|                 const type = this.openmct.types.get(item.type); | ||||
|                 if (type && type.definition) { | ||||
|                     creatable = (type.definition.creatable === true); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return creatable; | ||||
|         }, | ||||
|         hasConditionalStyle(domainObject, layoutItem) { | ||||
|             const id = layoutItem ? layoutItem.id : undefined; | ||||
|  | ||||
| @@ -251,7 +263,7 @@ export default { | ||||
|                 } else { | ||||
|                     this.canHide = true; | ||||
|                     domainObject = selectionItem[1].context.item; | ||||
|                     if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) { | ||||
|                     if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) { | ||||
|                         subObjects.push(item); | ||||
|                         itemStyle = getApplicableStylesForItem(item); | ||||
|                         if (this.hasConditionalStyle(item)) { | ||||
|   | ||||
| @@ -146,6 +146,8 @@ describe('the plugin', function () { | ||||
|         let displayLayoutItem; | ||||
|         let lineLayoutItem; | ||||
|         let boxLayoutItem; | ||||
|         let notCreatableObjectItem; | ||||
|         let notCreatableObject; | ||||
|         let selection; | ||||
|         let component; | ||||
|         let styleViewComponentObject; | ||||
| @@ -264,6 +266,19 @@ describe('the plugin', function () { | ||||
|                             "stroke": "#717171", | ||||
|                             "type": "line-view", | ||||
|                             "id": "57d49a28-7863-43bd-9593-6570758916f0" | ||||
|                         }, | ||||
|                         { | ||||
|                             "width": 32, | ||||
|                             "height": 18, | ||||
|                             "x": 36, | ||||
|                             "y": 8, | ||||
|                             "identifier": { | ||||
|                                 "key": "~TEST~image", | ||||
|                                 "namespace": "test-space" | ||||
|                             }, | ||||
|                             "hasFrame": true, | ||||
|                             "type": "subobject-view", | ||||
|                             "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" | ||||
|                         } | ||||
|                     ], | ||||
|                     "layoutGrid": [ | ||||
| @@ -297,6 +312,52 @@ describe('the plugin', function () { | ||||
|                 "type": "box-view", | ||||
|                 "id": "89b88746-d325-487b-aec4-11b79afff9e8" | ||||
|             }; | ||||
|             notCreatableObjectItem = { | ||||
|                 "width": 32, | ||||
|                 "height": 18, | ||||
|                 "x": 36, | ||||
|                 "y": 8, | ||||
|                 "identifier": { | ||||
|                     "key": "~TEST~image", | ||||
|                     "namespace": "test-space" | ||||
|                 }, | ||||
|                 "hasFrame": true, | ||||
|                 "type": "subobject-view", | ||||
|                 "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" | ||||
|             }; | ||||
|             notCreatableObject = { | ||||
|                 "identifier": { | ||||
|                     "key": "~TEST~image", | ||||
|                     "namespace": "test-space" | ||||
|                 }, | ||||
|                 "name": "test~image", | ||||
|                 "location": "test-space:~TEST", | ||||
|                 "type": "test.image", | ||||
|                 "telemetry": { | ||||
|                     "values": [ | ||||
|                         { | ||||
|                             "key": "value", | ||||
|                             "name": "Value", | ||||
|                             "hints": { | ||||
|                                 "image": 1, | ||||
|                                 "priority": 0 | ||||
|                             }, | ||||
|                             "format": "image", | ||||
|                             "source": "value" | ||||
|                         }, | ||||
|                         { | ||||
|                             "key": "utc", | ||||
|                             "source": "timestamp", | ||||
|                             "name": "Timestamp", | ||||
|                             "format": "iso", | ||||
|                             "hints": { | ||||
|                                 "domain": 1, | ||||
|                                 "priority": 1 | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             }; | ||||
|             selection = [ | ||||
|                 [{ | ||||
|                     context: { | ||||
| @@ -316,6 +377,19 @@ describe('the plugin', function () { | ||||
|                         "index": 0 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     context: { | ||||
|                         item: displayLayoutItem, | ||||
|                         "supportsMultiSelect": true | ||||
|                     } | ||||
|                 }], | ||||
|                 [{ | ||||
|                     context: { | ||||
|                         "item": notCreatableObject, | ||||
|                         "layoutItem": notCreatableObjectItem, | ||||
|                         "index": 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     context: { | ||||
|                         item: displayLayoutItem, | ||||
| @@ -344,7 +418,7 @@ describe('the plugin', function () { | ||||
|         }); | ||||
|  | ||||
|         it('initializes the items in the view', () => { | ||||
|             expect(styleViewComponentObject.items.length).toBe(2); | ||||
|             expect(styleViewComponentObject.items.length).toBe(3); | ||||
|         }); | ||||
|  | ||||
|         it('initializes conditional styles', () => { | ||||
| @@ -363,7 +437,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|             return Vue.nextTick().then(() => { | ||||
|                 expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); | ||||
|                 [boxLayoutItem, lineLayoutItem].forEach((item) => { | ||||
|                 [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { | ||||
|                     const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; | ||||
|                     expect(itemStyles.length).toBe(2); | ||||
|                     const foundStyle = itemStyles.find((style) => { | ||||
| @@ -385,7 +459,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|             return Vue.nextTick().then(() => { | ||||
|                 expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); | ||||
|                 [boxLayoutItem, lineLayoutItem].forEach((item) => { | ||||
|                 [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { | ||||
|                     const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; | ||||
|                     expect(itemStyle).toBeDefined(); | ||||
|                     const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); | ||||
|   | ||||
| @@ -64,9 +64,16 @@ define([ | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view></alphanumeric-format-view>' | ||||
|                             template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.alphanumericFormatView.getViewContext(); | ||||
|                         } else { | ||||
|                             return {}; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function () { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|   | ||||
| @@ -623,6 +623,33 @@ define(['lodash'], function (_) { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function getToggleGridButton(selection, selectionPath) { | ||||
|                     const ICON_GRID_SHOW = 'icon-grid-on'; | ||||
|                     const ICON_GRID_HIDE = 'icon-grid-off'; | ||||
|  | ||||
|                     let displayLayoutContext; | ||||
|  | ||||
|                     if (selection.length === 1 && selectionPath === undefined) { | ||||
|                         displayLayoutContext = selection[0][0].context; | ||||
|                     } else { | ||||
|                         displayLayoutContext = selectionPath[1].context; | ||||
|                     } | ||||
|  | ||||
|                     return { | ||||
|                         control: "button", | ||||
|                         domainObject: displayLayoutContext.item, | ||||
|                         icon: ICON_GRID_SHOW, | ||||
|                         method: function () { | ||||
|                             displayLayoutContext.toggleGrid(); | ||||
|  | ||||
|                             this.icon = this.icon === ICON_GRID_SHOW | ||||
|                                 ? ICON_GRID_HIDE | ||||
|                                 : ICON_GRID_SHOW; | ||||
|                         }, | ||||
|                         secondary: true | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 function getSeparator() { | ||||
|                     return { | ||||
|                         control: "separator" | ||||
| @@ -637,7 +664,9 @@ define(['lodash'], function (_) { | ||||
|                 } | ||||
|  | ||||
|                 if (isMainLayoutSelected(selectedObjects[0])) { | ||||
|                     return [getAddButton(selectedObjects)]; | ||||
|                     return [ | ||||
|                         getToggleGridButton(selectedObjects), | ||||
|                         getAddButton(selectedObjects)]; | ||||
|                 } | ||||
|  | ||||
|                 let toolbar = { | ||||
| @@ -653,7 +682,8 @@ define(['lodash'], function (_) { | ||||
|                     'position': [], | ||||
|                     'duplicate': [], | ||||
|                     'unit-toggle': [], | ||||
|                     'remove': [] | ||||
|                     'remove': [], | ||||
|                     'toggle-grid': [] | ||||
|                 }; | ||||
|  | ||||
|                 selectedObjects.forEach(selectionPath => { | ||||
| @@ -800,6 +830,10 @@ define(['lodash'], function (_) { | ||||
|                     if (toolbar.duplicate.length === 0) { | ||||
|                         toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)]; | ||||
|                     } | ||||
|  | ||||
|                     if (toolbar['toggle-grid'].length === 0) { | ||||
|                         toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 let toolbarArray = Object.values(toolbar); | ||||
|   | ||||
| @@ -56,6 +56,28 @@ define(function () { | ||||
|                         1 | ||||
|                     ], | ||||
|                     required: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Horizontal size (px)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     property: [ | ||||
|                         "configuration", | ||||
|                         "layoutDimensions", | ||||
|                         0 | ||||
|                     ], | ||||
|                     required: false | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Vertical size (px)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     property: [ | ||||
|                         "configuration", | ||||
|                         "layoutDimensions", | ||||
|                         1 | ||||
|                     ], | ||||
|                     required: false | ||||
|                 } | ||||
|             ] | ||||
|         }; | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import clipboard from '@/utils/clipboard'; | ||||
|  | ||||
| export default class CopyToClipboardAction { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy to Clipboard action'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToClipboard'; | ||||
|         this.name = 'Copy to Clipboard'; | ||||
|         this.priority = 9; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, viewContext) { | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
|                 this.openmct.notifications.info(`Success : copied to clipboard '${formattedValue}'`); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|                 this.openmct.notifications.error(`Failed : to copy to clipboard '${formattedValue}'`); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, viewContext) { | ||||
|         if (viewContext && viewContext.getViewKey) { | ||||
|             return viewContext.getViewKey().includes('alphanumeric-format'); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @@ -31,21 +31,19 @@ | ||||
|     @click.capture="bypassSelection" | ||||
|     @drop="handleDrop" | ||||
| > | ||||
|     <!-- Background grid --> | ||||
|     <div | ||||
|     <display-layout-grid | ||||
|         v-if="isEditing" | ||||
|         class="l-layout__grid-holder c-grid" | ||||
|         :grid-size="gridSize" | ||||
|         :show-grid="showGrid" | ||||
|     /> | ||||
|     <div | ||||
|         v-if="shouldDisplayLayoutDimensions" | ||||
|         class="l-layout__dimensions" | ||||
|         :style="layoutDimensionsStyle" | ||||
|     > | ||||
|         <div | ||||
|             v-if="gridSize[0] >= 3" | ||||
|             class="c-grid__x l-grid l-grid-x" | ||||
|             :style="[{ backgroundSize: gridSize[0] + 'px 100%' }]" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="gridSize[1] >= 3" | ||||
|             class="c-grid__y l-grid l-grid-y" | ||||
|             :style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]" | ||||
|         ></div> | ||||
|         <div class="l-layout__dimensions-vals"> | ||||
|             {{ layoutDimensions[0] }},{{ layoutDimensions[1] }} | ||||
|         </div> | ||||
|     </div> | ||||
|     <component | ||||
|         :is="item.type" | ||||
| @@ -81,6 +79,7 @@ import TextView from './TextView.vue'; | ||||
| import LineView from './LineView.vue'; | ||||
| import ImageView from './ImageView.vue'; | ||||
| import EditMarquee from './EditMarquee.vue'; | ||||
| import DisplayLayoutGrid from './DisplayLayoutGrid.vue'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| const TELEMETRY_IDENTIFIER_FUNCTIONS = { | ||||
| @@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3; | ||||
|  | ||||
| let components = ITEM_TYPE_VIEW_MAP; | ||||
| components['edit-marquee'] = EditMarquee; | ||||
| components['display-layout-grid'] = DisplayLayoutGrid; | ||||
|  | ||||
| function getItemDefinition(itemType, ...options) { | ||||
|     let itemView = ITEM_TYPE_VIEW_MAP[itemType]; | ||||
| @@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) { | ||||
|  | ||||
| export default { | ||||
|     components: components, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -156,7 +157,8 @@ export default { | ||||
|         return { | ||||
|             internalDomainObject: domainObject, | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [] | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -171,6 +173,23 @@ export default { | ||||
|                 return this.itemIsInCurrentSelection(item); | ||||
|             }); | ||||
|         }, | ||||
|         layoutDimensions() { | ||||
|             return this.internalDomainObject.configuration.layoutDimensions; | ||||
|         }, | ||||
|         shouldDisplayLayoutDimensions() { | ||||
|             return this.layoutDimensions | ||||
|                 && this.layoutDimensions[0] > 0 | ||||
|                 && this.layoutDimensions[1] > 0; | ||||
|         }, | ||||
|         layoutDimensionsStyle() { | ||||
|             const width = `${this.layoutDimensions[0]}px`; | ||||
|             const height = `${this.layoutDimensions[1]}px`; | ||||
|  | ||||
|             return { | ||||
|                 width, | ||||
|                 height | ||||
|             }; | ||||
|         }, | ||||
|         showMarquee() { | ||||
|             let selectionPath = this.selection[0]; | ||||
|             let singleSelectedLine = this.selection.length === 1 | ||||
| @@ -179,7 +198,13 @@ export default { | ||||
|             return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine; | ||||
|         } | ||||
|     }, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     watch: { | ||||
|         isEditing(value) { | ||||
|             if (value) { | ||||
|                 this.showGrid = value; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) { | ||||
|             this.internalDomainObject = JSON.parse(JSON.stringify(obj)); | ||||
| @@ -798,6 +823,9 @@ export default { | ||||
|  | ||||
|             this.removeItem(selection); | ||||
|             this.initSelectIndex = this.layoutItems.length - 1; //restore selection | ||||
|         }, | ||||
|         toggleGrid() { | ||||
|             this.showGrid = !this.showGrid; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/plugins/displayLayout/components/DisplayLayoutGrid.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/plugins/displayLayout/components/DisplayLayoutGrid.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="l-layout__grid-holder" | ||||
|     :class="{ 'c-grid': showGrid }" | ||||
| > | ||||
|     <div | ||||
|         v-if="gridSize[0] >= 3" | ||||
|         class="c-grid__x l-grid l-grid-x" | ||||
|         :style="[{ backgroundSize: gridSize[0] + 'px 100%' }]" | ||||
|     ></div> | ||||
|     <div | ||||
|         v-if="gridSize[1] >= 3" | ||||
|         class="c-grid__y l-grid l-grid-y" | ||||
|         :style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     props: { | ||||
|         gridSize: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             validator: (arr) => arr && arr.length === 2 | ||||
|                 && arr.every(el => typeof el === 'number') | ||||
|         }, | ||||
|         showGrid: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -138,14 +138,18 @@ export default { | ||||
|             this.domainObject = domainObject; | ||||
|             this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); | ||||
|             this.$nextTick(() => { | ||||
|                 let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                 childContext.item = domainObject; | ||||
|                 childContext.layoutItem = this.item; | ||||
|                 childContext.index = this.index; | ||||
|                 this.context = childContext; | ||||
|                 this.removeSelectable = this.openmct.selection.selectable( | ||||
|                     this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                 delete this.immediatelySelect; | ||||
|                 let reference = this.$refs.objectFrame; | ||||
|  | ||||
|                 if (reference) { | ||||
|                     let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                     childContext.item = domainObject; | ||||
|                     childContext.layoutItem = this.item; | ||||
|                     childContext.index = this.index; | ||||
|                     this.context = childContext; | ||||
|                     this.removeSelectable = this.openmct.selection.selectable( | ||||
|                         this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                     delete this.immediatelySelect; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -31,15 +31,12 @@ | ||||
|     <div | ||||
|         v-if="domainObject" | ||||
|         class="c-telemetry-view" | ||||
|         :class="{ | ||||
|             styleClass, | ||||
|             'is-missing': domainObject.status === 'missing' | ||||
|         }" | ||||
|         :class="[styleClass]" | ||||
|         :style="styleObject" | ||||
|         @contextmenu.prevent="showContextMenu" | ||||
|         @contextmenu.prevent.stop="showContextMenu" | ||||
|     > | ||||
|         <div class="is-missing__indicator" | ||||
|              title="This item is missing" | ||||
|         <div class="is-status__indicator" | ||||
|              title="This item is missing or suspect" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="showLabel" | ||||
| @@ -74,10 +71,11 @@ | ||||
| import LayoutFrame from './LayoutFrame.vue'; | ||||
| import printj from 'printj'; | ||||
| import conditionalStylesMixin from "../mixins/objectStyles-mixin"; | ||||
| import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js'; | ||||
|  | ||||
| const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; | ||||
| const DEFAULT_POSITION = [1, 1]; | ||||
| const CONTEXT_MENU_ACTIONS = ['viewHistoricalData']; | ||||
| const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData']; | ||||
|  | ||||
| export default { | ||||
|     makeDefinition(openmct, gridSize, domainObject, position) { | ||||
| @@ -126,13 +124,18 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentObjectPath: undefined, | ||||
|             datum: undefined, | ||||
|             formats: undefined, | ||||
|             domainObject: undefined, | ||||
|             currentObjectPath: undefined | ||||
|             formats: undefined, | ||||
|             viewKey: `alphanumeric-format-${Math.random()}`, | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         statusClass() { | ||||
|             return (this.status) ? `is-status--${this.status}` : ''; | ||||
|         }, | ||||
|         showLabel() { | ||||
|             let displayMode = this.item.displayMode; | ||||
|  | ||||
| @@ -205,9 +208,13 @@ export default { | ||||
|         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); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.removeSubscription(); | ||||
|         this.removeStatusListener(); | ||||
|  | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
| @@ -216,6 +223,18 @@ export default { | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|     }, | ||||
|     methods: { | ||||
|         getViewContext() { | ||||
|             return { | ||||
|                 getViewKey: () => this.viewKey, | ||||
|                 formattedValueForCopy: this.formattedValueForCopy | ||||
|             }; | ||||
|         }, | ||||
|         formattedValueForCopy() { | ||||
|             const timeFormatterKey = this.openmct.time.timeSystem().key; | ||||
|             const timeFormatter = this.formats[timeFormatterKey]; | ||||
|  | ||||
|             return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`; | ||||
|         }, | ||||
|         requestHistoricalData() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             let options = { | ||||
| @@ -253,6 +272,16 @@ export default { | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext() { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         skipCache: true | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
| @@ -276,12 +305,40 @@ export default { | ||||
|             this.removeSelectable = this.openmct.selection.selectable( | ||||
|                 this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|             delete this.immediatelySelect; | ||||
|  | ||||
|             let allActions = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|  | ||||
|             this.applicableActions = CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return allActions[actionKey]; | ||||
|             }); | ||||
|         }, | ||||
|         updateTelemetryFormat(format) { | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|         async getContextMenuActions() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); | ||||
|  | ||||
|             const actionsObject = this.openmct.actions.get(this.currentObjectPath, this.getViewContext(), { viewHistoricalData: true }).applicableActions; | ||||
|             let applicableActionKeys = Object.keys(actionsObject) | ||||
|                 .filter(key => { | ||||
|                     const isCopyToNotebook = actionsObject[key].key === 'copyToNotebook'; | ||||
|                     if (defaultNotebook && isCopyToNotebook) { | ||||
|                         const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                         actionsObject[key].name = `Copy to Notebook ${defaultPath}`; | ||||
|                     } | ||||
|  | ||||
|                     return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key); | ||||
|                 }); | ||||
|  | ||||
|             return applicableActionKeys.map(key => actionsObject[key]); | ||||
|         }, | ||||
|         async showContextMenu(event) { | ||||
|             const contextMenuActions = await this.getContextMenuActions(); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -17,10 +17,29 @@ | ||||
|     flex-direction: column; | ||||
|     overflow: auto; | ||||
|  | ||||
|     &__grid-holder { | ||||
|     &__grid-holder, | ||||
|     &__dimensions { | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     &__dimensions { | ||||
|         $b: 1px dashed $editDimensionsColor; | ||||
|         border-right: $b; | ||||
|         border-bottom: $b; | ||||
|         pointer-events: none; | ||||
|         position: absolute; | ||||
|  | ||||
|         &-vals { | ||||
|             $p: 2px; | ||||
|             color: $editDimensionsColor; | ||||
|             display: inline-block; | ||||
|             font-style: italic; | ||||
|             position: absolute; | ||||
|             bottom: $p; right: $p; | ||||
|             opacity: 0.7; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__frame { | ||||
|         position: absolute; | ||||
|     } | ||||
| @@ -34,6 +53,10 @@ | ||||
|             > .l-layout { | ||||
|                 background: $editUIGridColorBg; | ||||
|  | ||||
|                 > [class*="__dimensions"] { | ||||
|                     display: block; | ||||
|                 } | ||||
|  | ||||
|                 > [class*="__grid-holder"] { | ||||
|                     display: block; | ||||
|                 } | ||||
| @@ -42,12 +65,16 @@ | ||||
|     } | ||||
|  | ||||
|     .l-layout__frame { | ||||
|         &[s-selected], | ||||
|         &[s-selected]:not([multi-select="true"]), | ||||
|         &[s-selected-parent] { | ||||
|             // Display grid and allow edit marquee to display in nested layouts when editing | ||||
|             > * > * > .l-layout + .allow-editing { | ||||
|             > * > * > .l-layout.allow-editing { | ||||
|                 box-shadow: inset $editUIGridColorFg 0 0 2px 1px; | ||||
|  | ||||
|                 > [class*="__dimensions"] { | ||||
|                     display: block; | ||||
|                 } | ||||
|  | ||||
|                 > [class*='grid-holder'] { | ||||
|                     display: block; | ||||
|                 } | ||||
|   | ||||
| @@ -29,12 +29,12 @@ | ||||
|  | ||||
|     @include isMissing($absPos: true); | ||||
|  | ||||
|     .is-missing__indicator { | ||||
|     .is-status__indicator { | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|     } | ||||
|  | ||||
|     &.is-missing { | ||||
|     &.is-status--missing { | ||||
|         border: $borderMissing; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,9 +26,12 @@ import objectUtils from 'objectUtils'; | ||||
| import DisplayLayoutType from './DisplayLayoutType.js'; | ||||
| import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; | ||||
| import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; | ||||
| import CopyToClipboardAction from './actions/CopyToClipboardAction'; | ||||
|  | ||||
| export default function DisplayLayoutPlugin(options) { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new CopyToClipboardAction(openmct)); | ||||
|  | ||||
|         openmct.objectViews.addProvider({ | ||||
|             key: 'layout.view', | ||||
|             canView: function (domainObject) { | ||||
| @@ -72,7 +75,8 @@ export default function DisplayLayoutPlugin(options) { | ||||
|                             duplicateItem: component && component.$refs.displayLayout.duplicateItem, | ||||
|                             switchViewType: component && component.$refs.displayLayout.switchViewType, | ||||
|                             mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews, | ||||
|                             mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots | ||||
|                             mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots, | ||||
|                             toggleGrid: component && component.$refs.displayLayout.toggleGrid | ||||
|                         }; | ||||
|                     }, | ||||
|                     onEditModeChange: function (isEditing) { | ||||
|   | ||||
| @@ -340,7 +340,8 @@ describe('the plugin', function () { | ||||
|  | ||||
|         it('provides controls including separators', () => { | ||||
|             const displayLayoutToolbar = openmct.toolbars.get(selection); | ||||
|             expect(displayLayoutToolbar.length).toBe(9); | ||||
|  | ||||
|             expect(displayLayoutToolbar.length).toBe(11); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -3,20 +3,26 @@ | ||||
|     @include userSelectNone(); | ||||
|     background: $colorFilterBg; | ||||
|     color: $colorFilterFg; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-size: 0.9em; | ||||
|     margin-top: $interiorMarginSm; | ||||
|     padding: 2px; | ||||
|     text-transform: uppercase; | ||||
|  | ||||
|     &:before { | ||||
|         font-family: symbolsfont-12px; | ||||
|         content: $glyph-icon-filter; | ||||
|         display: block; | ||||
|         font-size: 12px; | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     &--mixed { | ||||
|         .c-filter-indication__mixed { | ||||
|             font-style: italic; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__label { | ||||
|         + .c-filter-indication__label { | ||||
|             &:before { | ||||
|                 content: ', '; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-filter-tree-item { | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| <template> | ||||
| <a | ||||
|     class="l-grid-view__item c-grid-item" | ||||
|     :class="{ | ||||
|     :class="[{ | ||||
|         'is-alias': item.isAlias === true, | ||||
|         'is-missing': item.model.status === 'missing', | ||||
|         'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 | ||||
|     }" | ||||
|     }, statusClass]" | ||||
|     :href="objectLink" | ||||
| > | ||||
|     <div | ||||
| @@ -27,8 +26,8 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="c-grid-item__controls"> | ||||
|         <div class="is-missing__indicator" | ||||
|              title="This item is missing" | ||||
|         <div class="is-status__indicator" | ||||
|              title="This item is missing or suspect" | ||||
|         ></div> | ||||
|         <div | ||||
|             class="icon-people" | ||||
| @@ -46,9 +45,10 @@ | ||||
| <script> | ||||
| import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
| import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink], | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -8,18 +8,18 @@ | ||||
|         <a | ||||
|             ref="objectLink" | ||||
|             class="c-object-label" | ||||
|             :class="{ 'is-missing': item.model.status === 'missing' }" | ||||
|             :class="[statusClass]" | ||||
|             :href="objectLink" | ||||
|         > | ||||
|             <div | ||||
|                 class="c-object-label__type-icon c-list-item__type-icon" | ||||
|                 class="c-object-label__type-icon c-list-item__name__type-icon" | ||||
|                 :class="item.type.cssClass" | ||||
|             > | ||||
|                 <span class="is-missing__indicator" | ||||
|                       title="This item is missing" | ||||
|                 <span class="is-status__indicator" | ||||
|                       title="This item is missing or suspect" | ||||
|                 ></span> | ||||
|             </div> | ||||
|             <div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div> | ||||
|             <div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td class="c-list-item__type"> | ||||
| @@ -39,9 +39,10 @@ | ||||
| import moment from 'moment'; | ||||
| import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
| import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink], | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-missing { | ||||
|     &.is-status--missing { | ||||
|         @include isMissing(); | ||||
|  | ||||
|         [class*='__type-icon'], | ||||
|   | ||||
| @@ -1,11 +1,19 @@ | ||||
| /******************************* LIST ITEM */ | ||||
| .c-list-item { | ||||
|     &__type-icon { | ||||
|     &__name__type-icon { | ||||
|         color: $colorItemTreeIcon; | ||||
|     } | ||||
|  | ||||
|     &__name { | ||||
|     &__name__name { | ||||
|         @include ellipsize(); | ||||
|  | ||||
|         a & { | ||||
|             color: $colorItemFg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &:not(.c-list-item__name) { | ||||
|         color: $colorItemFgDetails; | ||||
|     } | ||||
|  | ||||
|     &.is-alias { | ||||
|   | ||||
| @@ -28,9 +28,5 @@ | ||||
|         padding-top: $p; | ||||
|         padding-bottom: $p; | ||||
|         width: 25%; | ||||
|  | ||||
|         &:not(.c-list-item__name) { | ||||
|             color: $colorItemFgDetails; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/plugins/folderView/components/status-listener.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/plugins/folderView/components/status-listener.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         statusClass() { | ||||
|             return (this.status) ? `is-status--${this.status}` : ''; | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         let identifier = this.item.model.identifier; | ||||
|  | ||||
|         this.status = this.openmct.status.get(identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.removeStatusListener(); | ||||
|     } | ||||
| }; | ||||
| @@ -25,6 +25,8 @@ export default class GoToOriginalAction { | ||||
|         this.name = 'Go To Original'; | ||||
|         this.key = 'goToOriginal'; | ||||
|         this.description = 'Go to the original unlinked instance of this object'; | ||||
|         this.group = 'action'; | ||||
|         this.priority = 4; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|     } | ||||
|   | ||||
| @@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction'; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.contextMenu.registerAction(new GoToOriginalAction(openmct)); | ||||
|         openmct.actions.register(new GoToOriginalAction(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| <template> | ||||
| <div class="c-imagery"> | ||||
| <div | ||||
|     tabindex="0" | ||||
|     class="c-imagery" | ||||
|     @keyup="arrowUpHandler" | ||||
|     @keydown="arrowDownHandler" | ||||
|     @mouseover="focusElement" | ||||
| > | ||||
|     <div class="c-imagery__main-image-wrapper has-local-controls"> | ||||
|         <div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc"> | ||||
|             <span class="holder flex-elem grows c-imagery__lc__sliders"> | ||||
| @@ -23,94 +29,187 @@ | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="main-image s-image-main c-imagery__main-image has-local-controls" | ||||
|              :class="{'paused unnsynced': paused(),'stale':false }" | ||||
|              :style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none', | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|              :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 class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> | ||||
|                 <button class="c-nav c-nav--prev" | ||||
|                         title="Previous image" | ||||
|                         :disabled="isPrevDisabled()" | ||||
|                         :disabled="isPrevDisabled" | ||||
|                         @click="prevImage()" | ||||
|                 ></button> | ||||
|                 <button class="c-nav c-nav--next" | ||||
|                         title="Next image" | ||||
|                         :disabled="isNextDisabled()" | ||||
|                         :disabled="isNextDisabled" | ||||
|                         @click="nextImage()" | ||||
|                 ></button> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="c-imagery__control-bar"> | ||||
|             <div class="c-imagery__timestamp">{{ getTime() }}</div> | ||||
|             <div class="c-imagery__time"> | ||||
|                 <div class="c-imagery__timestamp">{{ time }}</div> | ||||
|                 <div | ||||
|                     v-if="canTrackDuration" | ||||
|                     :class="{'c-imagery--new': isImageNew && !refreshCSS}" | ||||
|                     class="c-imagery__age icon-timer" | ||||
|                 >{{ formattedDuration }}</div> | ||||
|             </div> | ||||
|             <div class="h-local-controls flex-elem"> | ||||
|                 <button | ||||
|                     class="c-button icon-pause pause-play" | ||||
|                     :class="{'is-paused': paused()}" | ||||
|                     @click="paused(!paused(), true)" | ||||
|                     :class="{'is-paused': isPaused}" | ||||
|                     @click="paused(!isPaused, 'button')" | ||||
|                 ></button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div ref="thumbsWrapper" | ||||
|          class="c-imagery__thumbs-wrapper" | ||||
|          :class="{'is-paused': paused()}" | ||||
|          :class="{'is-paused': isPaused}" | ||||
|          @scroll="handleScroll" | ||||
|     > | ||||
|         <div v-for="(imageData, index) in imageHistory" | ||||
|              :key="index" | ||||
|         <div v-for="(datum, index) in imageHistory" | ||||
|              :key="datum.url" | ||||
|              class="c-imagery__thumb c-thumb" | ||||
|              :class="{selected: imageData.selected}" | ||||
|              @click="setSelectedImage(imageData)" | ||||
|              :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|              @click="setFocusedImage(index, thumbnailClick)" | ||||
|         > | ||||
|             <img class="c-thumb__image" | ||||
|                  :src="getImageUrl(imageData)" | ||||
|                  :src="formatImageUrl(datum)" | ||||
|             > | ||||
|             <div class="c-thumb__timestamp">{{ getTime(imageData) }}</div> | ||||
|             <div class="c-thumb__timestamp">{{ formatTime(datum) }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'moment'; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| const DURATION_TRACK_MS = 1000; | ||||
| const ARROW_DOWN_DELAY_CHECK_MS = 400; | ||||
| const ARROW_SCROLL_RATE_MS = 100; | ||||
| const THUMBNAIL_CLICKED = true; | ||||
|  | ||||
| const ONE_MINUTE = 60 * 1000; | ||||
| const FIVE_MINUTES = 5 * ONE_MINUTE; | ||||
| const ONE_HOUR = ONE_MINUTE * 60; | ||||
| const EIGHT_HOURS = 8 * ONE_HOUR; | ||||
| const TWENTYFOUR_HOURS = EIGHT_HOURS * 3; | ||||
|  | ||||
| const ARROW_RIGHT = 39; | ||||
| const ARROW_LEFT = 37; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|  | ||||
|         return { | ||||
|             autoScroll: true, | ||||
|             durationFormatter: undefined, | ||||
|             filters: { | ||||
|                 brightness: 100, | ||||
|                 contrast: 100 | ||||
|             }, | ||||
|             image: { | ||||
|                 selected: '' | ||||
|             }, | ||||
|             imageFormat: '', | ||||
|             imageHistory: [], | ||||
|             imageUrl: '', | ||||
|             thumbnailClick: THUMBNAIL_CLICKED, | ||||
|             isPaused: false, | ||||
|             metadata: {}, | ||||
|             requestCount: 0, | ||||
|             timeFormat: '' | ||||
|             timeSystem: timeSystem, | ||||
|             timeFormatter: undefined, | ||||
|             refreshCSS: false, | ||||
|             keyString: undefined, | ||||
|             focusedImageIndex: undefined, | ||||
|             numericDuration: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         bounds() { | ||||
|             return this.openmct.time.bounds(); | ||||
|         time() { | ||||
|             return this.formatTime(this.focusedImage); | ||||
|         }, | ||||
|         imageUrl() { | ||||
|             return this.formatImageUrl(this.focusedImage); | ||||
|         }, | ||||
|         isImageNew() { | ||||
|             let cutoff = FIVE_MINUTES; | ||||
|             let age = this.numericDuration; | ||||
|  | ||||
|             return age < cutoff && !this.refreshCSS; | ||||
|         }, | ||||
|         canTrackDuration() { | ||||
|             return this.openmct.time.clock() && this.timeSystem.isUTCBased; | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
|  | ||||
|             if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) { | ||||
|                 disabled = true; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         }, | ||||
|         isPrevDisabled() { | ||||
|             let disabled = false; | ||||
|  | ||||
|             if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) { | ||||
|                 disabled = true; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         }, | ||||
|         focusedImage() { | ||||
|             return this.imageHistory[this.focusedImageIndex]; | ||||
|         }, | ||||
|         parsedSelectedTime() { | ||||
|             return this.parseTime(this.focusedImage); | ||||
|         }, | ||||
|         formattedDuration() { | ||||
|             let result = 'N/A'; | ||||
|             let negativeAge = -1; | ||||
|  | ||||
|             if (this.numericDuration > TWENTYFOUR_HOURS) { | ||||
|                 negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS); | ||||
|                 result = moment.duration(negativeAge, 'days').humanize(true); | ||||
|             } else if (this.numericDuration > EIGHT_HOURS) { | ||||
|                 negativeAge *= (this.numericDuration / ONE_HOUR); | ||||
|                 result = moment.duration(negativeAge, 'hours').humanize(true); | ||||
|             } else if (this.durationFormatter) { | ||||
|                 result = this.durationFormatter.format(this.numericDuration); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         focusedImageIndex() { | ||||
|             this.trackDuration(); | ||||
|             this.resetAgeCSS(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         // set | ||||
|         this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); | ||||
|         // initialize | ||||
|         this.timeKey = this.openmct.time.timeSystem().key; | ||||
|         this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.on('clock', this.clockChange); | ||||
|  | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); | ||||
|  | ||||
|         // initialize | ||||
|         this.timeKey = this.timeSystem.key; | ||||
|         this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|  | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
| @@ -124,38 +223,55 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.stopDurationTracking(); | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.off('clock', this.clockChange); | ||||
|     }, | ||||
|     methods: { | ||||
|         focusElement() { | ||||
|             this.$el.focus(); | ||||
|         }, | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             const datumTime = this.timeFormat.format(datum); | ||||
|             const datumURL = this.imageFormat.format(datum); | ||||
|             const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]); | ||||
|             const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]); | ||||
|             const datumURL = this.formatImageUrl(datum); | ||||
|             const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); | ||||
|  | ||||
|             // datum is not valid if it matches the last datum in history, | ||||
|             // or it is before the last datum in the history | ||||
|             const datumTimeCheck = this.timeFormat.parse(datum); | ||||
|             const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL); | ||||
|             const datumTimeCheck = this.parseTime(datum); | ||||
|             const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); | ||||
|             const isStale = datumTimeCheck < historyTimeCheck; | ||||
|  | ||||
|             return matchesLast || isStale; | ||||
|         }, | ||||
|         getImageUrl(datum) { | ||||
|             return datum | ||||
|                 ? this.imageFormat.format(datum) | ||||
|                 : this.imageUrl; | ||||
|         formatImageUrl(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.imageFormatter.format(datum); | ||||
|         }, | ||||
|         getTime(datum) { | ||||
|             return datum | ||||
|                 ? this.timeFormat.format(datum) | ||||
|                 : this.time; | ||||
|         formatTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let dateTimeStr = this.timeFormatter.format(datum); | ||||
|  | ||||
|             // Replace ISO "T" with a space to allow wrapping | ||||
|             return dateTimeStr.replace("T", " "); | ||||
|         }, | ||||
|         parseTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.timeFormatter.parse(datum); | ||||
|         }, | ||||
|         handleScroll() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
| @@ -168,26 +284,35 @@ export default { | ||||
|                     || (scrollHeight - scrollTop) > 2 * clientHeight; | ||||
|             this.autoScroll = !disableScroll; | ||||
|         }, | ||||
|         paused(state, button = false) { | ||||
|             if (arguments.length > 0 && state !== this.isPaused) { | ||||
|                 this.unselectAllImages(); | ||||
|                 this.isPaused = state; | ||||
|                 if (state === true && button) { | ||||
|                     // If we are pausing, select the latest image in imageHistory | ||||
|                     this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]); | ||||
|                 } | ||||
|         paused(state, type) { | ||||
|  | ||||
|                 if (this.nextDatum) { | ||||
|                     this.updateValues(this.nextDatum); | ||||
|                     delete this.nextDatum; | ||||
|                 } else { | ||||
|                     this.updateValues(this.imageHistory[this.imageHistory.length - 1]); | ||||
|                 } | ||||
|             this.isPaused = state; | ||||
|  | ||||
|                 this.autoScroll = true; | ||||
|             if (type === 'button') { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|             } | ||||
|  | ||||
|             return this.isPaused; | ||||
|             if (this.nextImageIndex) { | ||||
|                 this.setFocusedImage(this.nextImageIndex); | ||||
|                 delete this.nextImageIndex; | ||||
|             } | ||||
|  | ||||
|             this.autoScroll = true; | ||||
|         }, | ||||
|         scrollToFocused() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
|             if (!thumbsWrapper) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let domThumb = thumbsWrapper.children[this.focusedImageIndex]; | ||||
|  | ||||
|             if (domThumb) { | ||||
|                 domThumb.scrollIntoView({ | ||||
|                     behavior: 'smooth', | ||||
|                     block: 'center' | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         scrollToRight() { | ||||
|             if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { | ||||
| @@ -201,22 +326,17 @@ export default { | ||||
|  | ||||
|             setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0); | ||||
|         }, | ||||
|         setSelectedImage(image) { | ||||
|             // If we are paused and the current image IS selected, unpause | ||||
|             // Otherwise, set current image and pause | ||||
|             if (!image) { | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             if (this.isPaused && !thumbnailClick) { | ||||
|                 this.nextImageIndex = index; | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isPaused && image.selected) { | ||||
|                 this.paused(false); | ||||
|                 this.unselectAllImages(); | ||||
|             } else { | ||||
|                 this.imageUrl = this.getImageUrl(image); | ||||
|                 this.time = this.getTime(image); | ||||
|             this.focusedImageIndex = index; | ||||
|  | ||||
|             if (thumbnailClick && !this.isPaused) { | ||||
|                 this.paused(true); | ||||
|                 this.unselectAllImages(); | ||||
|                 image.selected = true; | ||||
|             } | ||||
|         }, | ||||
|         boundsChange(bounds, isTick) { | ||||
| @@ -224,98 +344,158 @@ export default { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         requestHistory() { | ||||
|             const requestId = ++this.requestCount; | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
|             this.openmct.telemetry | ||||
|                 .request(this.domainObject, this.bounds) | ||||
|                 .then((values = []) => { | ||||
|                     if (this.requestCount === requestId) { | ||||
|                         // add each image to the history | ||||
|                         // update values for the very last image (set current image time and url) | ||||
|                         values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1)); | ||||
|                     } | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
|  | ||||
|             if (this.requestCount === requestId) { | ||||
|                 data.forEach((datum, index) => { | ||||
|                     this.updateHistory(datum, index === data.length - 1); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         timeSystemChange(system) { | ||||
|             // reset timesystem dependent variables | ||||
|             this.timeKey = system.key; | ||||
|             this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); | ||||
|             this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeKey = this.timeSystem.key; | ||||
|             this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|             this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         clockChange(clock) { | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         subscribe() { | ||||
|             this.unsubscribe = this.openmct.telemetry | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.timeFormat.parse(datum); | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
|  | ||||
|                     if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) { | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         this.updateHistory(datum); | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
|         unselectAllImages() { | ||||
|             this.imageHistory.forEach(image => image.selected = false); | ||||
|         }, | ||||
|         updateHistory(datum, updateValues = true) { | ||||
|         updateHistory(datum, setFocused = true) { | ||||
|             if (this.datumIsNotValid(datum)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.imageHistory.push(datum); | ||||
|  | ||||
|             if (updateValues) { | ||||
|                 this.updateValues(datum); | ||||
|             if (setFocused) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|             } | ||||
|         }, | ||||
|         updateValues(datum) { | ||||
|             if (this.isPaused) { | ||||
|                 this.nextDatum = datum; | ||||
|         getFormatter(key) { | ||||
|             let metadataValue = this.metadata.value(key) || { format: key }; | ||||
|             let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|             return valueFormatter; | ||||
|         }, | ||||
|         trackDuration() { | ||||
|             if (this.canTrackDuration) { | ||||
|                 this.stopDurationTracking(); | ||||
|                 this.updateDuration(); | ||||
|                 this.durationTracker = window.setInterval( | ||||
|                     this.updateDuration, DURATION_TRACK_MS | ||||
|                 ); | ||||
|             } else { | ||||
|                 this.stopDurationTracking(); | ||||
|             } | ||||
|         }, | ||||
|         stopDurationTracking() { | ||||
|             window.clearInterval(this.durationTracker); | ||||
|         }, | ||||
|         updateDuration() { | ||||
|             let currentTime = this.openmct.time.clock().currentValue(); | ||||
|             this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
|             this.refreshCSS = true; | ||||
|             // unable to make this work with nextTick | ||||
|             setTimeout(() => { | ||||
|                 this.refreshCSS = false; | ||||
|             }, REFRESH_CSS_MS); | ||||
|         }, | ||||
|         nextImage() { | ||||
|             if (this.isNextDisabled) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.time = this.timeFormat.format(datum); | ||||
|             this.imageUrl = this.imageFormat.format(datum); | ||||
|         }, | ||||
|         selectedImageIndex() { | ||||
|             return this.imageHistory.findIndex(image => image.selected); | ||||
|         }, | ||||
|         setSelectedByIndex(index) { | ||||
|             this.setSelectedImage(this.imageHistory[index]); | ||||
|         }, | ||||
|         nextImage() { | ||||
|             let index = this.selectedImageIndex(); | ||||
|             this.setSelectedByIndex(++index); | ||||
|             let index = this.focusedImageIndex; | ||||
|  | ||||
|             this.setFocusedImage(++index, THUMBNAIL_CLICKED); | ||||
|             if (index === this.imageHistory.length - 1) { | ||||
|                 this.paused(false); | ||||
|             } | ||||
|         }, | ||||
|         prevImage() { | ||||
|             let index = this.selectedImageIndex(); | ||||
|             if (index === -1) { | ||||
|                 this.setSelectedByIndex(this.imageHistory.length - 2); | ||||
|             if (this.isPrevDisabled) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let index = this.focusedImageIndex; | ||||
|  | ||||
|             if (index === this.imageHistory.length - 1) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED); | ||||
|             } else { | ||||
|                 this.setSelectedByIndex(--index); | ||||
|                 this.setFocusedImage(--index, THUMBNAIL_CLICKED); | ||||
|             } | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
|             let index = this.selectedImageIndex(); | ||||
|         arrowDownHandler(event) { | ||||
|             let key = event.keyCode; | ||||
|  | ||||
|             if (index === -1 || index === this.imageHistory.length - 1) { | ||||
|                 disabled = true; | ||||
|             if (this.isLeftOrRightArrowKey(key)) { | ||||
|                 this.arrowDown = true; | ||||
|                 window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|                 this.arrowDownDelayTimeout = window.setTimeout(() => { | ||||
|                     this.arrowKeyScroll(this.directionByKey(key)); | ||||
|                 }, ARROW_DOWN_DELAY_CHECK_MS); | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         }, | ||||
|         isPrevDisabled() { | ||||
|             let disabled = false; | ||||
|             let index = this.selectedImageIndex(); | ||||
|         arrowUpHandler(event) { | ||||
|             let key = event.keyCode; | ||||
|  | ||||
|             if (index === 0 || this.imageHistory.length < 2) { | ||||
|                 disabled = true; | ||||
|             window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|  | ||||
|             if (this.isLeftOrRightArrowKey(key)) { | ||||
|                 this.arrowDown = false; | ||||
|                 let direction = this.directionByKey(key); | ||||
|                 this[direction + 'Image'](); | ||||
|             } | ||||
|         }, | ||||
|         arrowKeyScroll(direction) { | ||||
|             if (this.arrowDown) { | ||||
|                 this.arrowKeyScrolling = true; | ||||
|                 this[direction + 'Image'](); | ||||
|                 setTimeout(() => { | ||||
|                     this.arrowKeyScroll(direction); | ||||
|                 }, ARROW_SCROLL_RATE_MS); | ||||
|             } else { | ||||
|                 window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|                 this.arrowKeyScrolling = false; | ||||
|                 this.scrollToFocused(); | ||||
|             } | ||||
|         }, | ||||
|         directionByKey(keyCode) { | ||||
|             let direction; | ||||
|  | ||||
|             if (keyCode === ARROW_LEFT) { | ||||
|                 direction = 'prev'; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|             if (keyCode === ARROW_RIGHT) { | ||||
|                 direction = 'next'; | ||||
|             } | ||||
|  | ||||
|             return direction; | ||||
|         }, | ||||
|         isLeftOrRightArrowKey(keyCode) { | ||||
|             return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -4,6 +4,10 @@ | ||||
|     overflow: hidden; | ||||
|     height: 100%; | ||||
|  | ||||
|     &:focus { | ||||
|         outline: none; | ||||
|     } | ||||
|  | ||||
|     > * + * { | ||||
|         margin-top: $interiorMargin; | ||||
|     } | ||||
| @@ -25,14 +29,57 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__control-bar { | ||||
|         padding: 5px 0 0 0; | ||||
|     &__control-bar, | ||||
|     &__time { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         align-items: baseline; | ||||
|  | ||||
|         > * + * { | ||||
|             margin-left: $interiorMarginSm; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     &__control-bar { | ||||
|         margin-top: 2px; | ||||
|         padding: $interiorMarginSm 0; | ||||
|         justify-content: space-between; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     &__time { | ||||
|         flex: 0 1 auto; | ||||
|         overflow: hidden; | ||||
|     } | ||||
|  | ||||
|     &__timestamp, | ||||
|     &__age { | ||||
|         @include ellipsize(); | ||||
|         flex: 0 1 auto; | ||||
|     } | ||||
|  | ||||
|     &__timestamp { | ||||
|         flex: 1 1 auto; | ||||
|         flex-shrink: 10; | ||||
|     } | ||||
|  | ||||
|     &__age { | ||||
|         border-radius: $controlCr; | ||||
|         display: flex; | ||||
|         flex-shrink: 0; | ||||
|         align-items: baseline; | ||||
|         padding: 1px $interiorMarginSm; | ||||
|  | ||||
|         &:before { | ||||
|             opacity: 0.5; | ||||
|             margin-right: $interiorMarginSm; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &--new { | ||||
|         // New imagery | ||||
|         $bgColor: $colorOk; | ||||
|         background: rgba($bgColor, 0.5); | ||||
|         @include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0)); | ||||
|     } | ||||
|  | ||||
|     &__thumbs-wrapper { | ||||
| @@ -151,6 +198,8 @@ | ||||
| /*************************************** BUTTONS */ | ||||
| .c-button.pause-play { | ||||
|     // Pause icon set by default in markup | ||||
|     justify-self: end; | ||||
|  | ||||
|     &.is-paused { | ||||
|         background: $colorPausedBg !important; | ||||
|         color: $colorPausedFg; | ||||
|   | ||||
| @@ -28,6 +28,8 @@ export default class NewFolderAction { | ||||
|         this.key = 'newFolder'; | ||||
|         this.description = 'Create a new folder'; | ||||
|         this.cssClass = 'icon-folder-new'; | ||||
|         this.group = "action"; | ||||
|         this.priority = 9; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._dialogForm = { | ||||
|   | ||||
| @@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction'; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.contextMenu.registerAction(new NewFolderAction(openmct)); | ||||
|         openmct.actions.register(new NewFolderAction(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -40,9 +40,7 @@ describe("the plugin", () => { | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         newFolderAction = openmct.contextMenu._allActions.filter(action => { | ||||
|             return action.key === 'newFolder'; | ||||
|         })[0]; | ||||
|         newFolderAction = openmct.actions._allActions.newFolder; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|   | ||||
							
								
								
									
										39
									
								
								src/plugins/notebook/actions/CopyToNotebookAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/plugins/notebook/actions/CopyToNotebookAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry } from '../utils/notebook-entries'; | ||||
|  | ||||
| export default class CopyToNotebookAction { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy to Notebook action'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToNotebook'; | ||||
|         this.name = 'Copy to Notebook'; | ||||
|         this.priority = 9; | ||||
|     } | ||||
|  | ||||
|     copyToNotebook(entryText) { | ||||
|         const notebookStorage = getDefaultNotebook(); | ||||
|         this.openmct.objects.get(notebookStorage.notebookMeta.identifier) | ||||
|             .then(domainObject => { | ||||
|                 addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText); | ||||
|  | ||||
|                 const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`; | ||||
|                 const msg = `Saved to Notebook ${defaultPath}`; | ||||
|                 this.openmct.notifications.info(msg); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, viewContext) { | ||||
|         this.copyToNotebook(viewContext.formattedValueForCopy()); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, viewContext) { | ||||
|         if (viewContext && viewContext.getViewKey) { | ||||
|             return viewContext.getViewKey().includes('alphanumeric-format'); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @@ -112,9 +112,9 @@ import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries'; | ||||
| import { throttle } from 'lodash'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| const DEFAULT_CLASS = 'is-notebook-default'; | ||||
| import { throttle } from 'lodash'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'snapshotContainer'], | ||||
| @@ -197,15 +197,6 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         addDefaultClass() { | ||||
|             const classList = this.internalDomainObject.classList || []; | ||||
|             if (classList.includes(DEFAULT_CLASS)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             classList.push(DEFAULT_CLASS); | ||||
|             this.mutateObject('classList', classList); | ||||
|         }, | ||||
|         changeSelectedSection({ sectionId, pageId }) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
| @@ -425,14 +416,7 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const classList = domainObject.classList || []; | ||||
|             const index = classList.indexOf(DEFAULT_CLASS); | ||||
|             if (!classList.length || index < 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             classList.splice(index, 1); | ||||
|             this.openmct.objects.mutate(domainObject, 'classList', classList); | ||||
|             this.openmct.status.delete(domainObject.identifier); | ||||
|         }, | ||||
|         searchItem(input) { | ||||
|             this.search = input; | ||||
| @@ -442,11 +426,20 @@ export default { | ||||
|         }, | ||||
|         async updateDefaultNotebook(notebookStorage) { | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             this.removeDefaultClass(defaultNotebookObject); | ||||
|             setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             this.addDefaultClass(); | ||||
|             this.defaultSectionId = notebookStorage.section.id; | ||||
|             this.defaultPageId = notebookStorage.page.id; | ||||
|             if (!defaultNotebookObject) { | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { | ||||
|                 this.removeDefaultClass(defaultNotebookObject); | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } | ||||
|  | ||||
|             if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { | ||||
|                 this.defaultSectionId = notebookStorage.section.id; | ||||
|             } | ||||
|  | ||||
|             if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) { | ||||
|                 this.defaultPageId = notebookStorage.page.id; | ||||
|             } | ||||
|         }, | ||||
|         updateDefaultNotebookPage(pages, id) { | ||||
|             if (!id) { | ||||
|   | ||||
| @@ -143,7 +143,8 @@ export default { | ||||
|                 this.openmct.notifications.alert(message); | ||||
|             } | ||||
|  | ||||
|             window.location.href = link; | ||||
|             const url = new URL(link); | ||||
|             window.location.href = url.hash; | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|   | ||||
| @@ -1,29 +1,17 @@ | ||||
| <template> | ||||
| <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> | ||||
|     <button | ||||
|         class="c-button--menu icon-notebook" | ||||
|         class="c-icon-button c-button--menu icon-camera" | ||||
|         title="Take a Notebook Snapshot" | ||||
|         @click="setNotebookTypes" | ||||
|         @click.stop="toggleMenu" | ||||
|         @click.stop.prevent="showMenu" | ||||
|     > | ||||
|         <span class="c-button__label"></span> | ||||
|         <span | ||||
|             title="Take Notebook Snapshot" | ||||
|             class="c-icon-button__label" | ||||
|         > | ||||
|             Snapshot | ||||
|         </span> | ||||
|     </button> | ||||
|     <div | ||||
|         v-show="showMenu" | ||||
|         class="c-menu" | ||||
|     > | ||||
|         <ul> | ||||
|             <li | ||||
|                 v-for="(type, index) in notebookTypes" | ||||
|                 :key="index" | ||||
|                 :class="type.cssClass" | ||||
|                 :title="type.name" | ||||
|                 @click="snapshot(type)" | ||||
|             > | ||||
|                 {{ type.name }} | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -57,22 +45,20 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             notebookSnapshot: null, | ||||
|             notebookTypes: [], | ||||
|             showMenu: false | ||||
|             notebookTypes: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.notebookSnapshot = new Snapshot(this.openmct); | ||||
|  | ||||
|         document.addEventListener('click', this.hideMenu); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         document.removeEventListener('click', this.hideMenu); | ||||
|         this.setDefaultNotebookStatus(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setNotebookTypes() { | ||||
|         showMenu(event) { | ||||
|             const notebookTypes = []; | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const elementBoundingClientRect = this.$el.getBoundingClientRect(); | ||||
|             const x = elementBoundingClientRect.x; | ||||
|             const y = elementBoundingClientRect.y + elementBoundingClientRect.height; | ||||
|  | ||||
|             if (defaultNotebook) { | ||||
|                 const domainObject = defaultNotebook.domainObject; | ||||
| @@ -83,35 +69,31 @@ export default { | ||||
|                     notebookTypes.push({ | ||||
|                         cssClass: 'icon-notebook', | ||||
|                         name: `Save to Notebook ${defaultPath}`, | ||||
|                         type: NOTEBOOK_DEFAULT | ||||
|                         callBack: () => { | ||||
|                             return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: 'icon-notebook', | ||||
|                 cssClass: 'icon-camera', | ||||
|                 name: 'Save to Notebook Snapshots', | ||||
|                 type: NOTEBOOK_SNAPSHOT | ||||
|                 callBack: () => { | ||||
|                     return this.snapshot(NOTEBOOK_SNAPSHOT); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this.notebookTypes = notebookTypes; | ||||
|         }, | ||||
|         toggleMenu() { | ||||
|             this.showMenu = !this.showMenu; | ||||
|         }, | ||||
|         hideMenu() { | ||||
|             this.showMenu = false; | ||||
|             this.openmct.menus.showMenu(x, y, notebookTypes); | ||||
|         }, | ||||
|         snapshot(notebook) { | ||||
|             this.hideMenu(); | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 const element = document.querySelector('.c-overlay__contents') | ||||
|                     || document.getElementsByClassName('l-shell__main-container')[0]; | ||||
|  | ||||
|                 const bounds = this.openmct.time.bounds(); | ||||
|                 const link = !this.ignoreLink | ||||
|                     ? window.location.href | ||||
|                     ? window.location.hash | ||||
|                     : null; | ||||
|  | ||||
|                 const objectPath = this.objectPath || this.openmct.router.path; | ||||
| @@ -124,6 +106,15 @@ export default { | ||||
|  | ||||
|                 this.notebookSnapshot.capture(snapshotMeta, notebook.type, element); | ||||
|             }); | ||||
|         }, | ||||
|         setDefaultNotebookStatus() { | ||||
|             let defaultNotebookObject = getDefaultNotebook(); | ||||
|  | ||||
|             if (defaultNotebookObject && defaultNotebookObject.notebookMeta) { | ||||
|                 let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier; | ||||
|  | ||||
|                 this.openmct.status.set(notebookIdentifier, 'notebook-default'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|         <div class="l-browse-bar__start"> | ||||
|             <div class="l-browse-bar__object-name--w"> | ||||
|                 <div class="l-browse-bar__object-name c-object-label"> | ||||
|                     <div class="c-object-label__type-icon icon-notebook"></div> | ||||
|                     <div class="c-object-label__type-icon icon-camera"></div> | ||||
|                     <div class="c-object-label__name"> | ||||
|                         Notebook Snapshots | ||||
|                         <span v-if="snapshots.length" | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="c-indicator c-indicator--clickable icon-notebook" | ||||
| <div class="c-indicator c-indicator--clickable icon-camera" | ||||
|      :class="[ | ||||
|          { 's-status-off': snapshotCount === 0 }, | ||||
|          { 's-status-on': snapshotCount > 0 }, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import CopyToNotebookAction from './actions/CopyToNotebookAction'; | ||||
| import Notebook from './components/Notebook.vue'; | ||||
| import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| @@ -13,6 +14,8 @@ export default function NotebookPlugin() { | ||||
|  | ||||
|         installed = true; | ||||
|  | ||||
|         openmct.actions.register(new CopyToNotebookAction(openmct)); | ||||
|  | ||||
|         const notebookType = { | ||||
|             name: 'Notebook', | ||||
|             description: 'Create and save timestamped notes with embedded object snapshots.', | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing'; | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import NotebookPlugin from './plugin'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| @@ -133,90 +133,4 @@ describe("Notebook plugin:", () => { | ||||
|             expect(hasMajorElements).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Notebook Snapshots view:", () => { | ||||
|         let snapshotIndicator; | ||||
|         let drawerElement; | ||||
|  | ||||
|         function clickSnapshotIndicator() { | ||||
|             const indicator = element.querySelector('.icon-notebook'); | ||||
|             const button = indicator.querySelector('button'); | ||||
|             const clickEvent = createMouseEvent('click'); | ||||
|  | ||||
|             button.dispatchEvent(clickEvent); | ||||
|         } | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             snapshotIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'notebook-snapshot-indicator').element; | ||||
|  | ||||
|             element.append(snapshotIndicator); | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             snapshotIndicator.remove(); | ||||
|             if (drawerElement) { | ||||
|                 drawerElement.remove(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             drawerElement = document.querySelector('.l-shell__drawer'); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             if (drawerElement) { | ||||
|                 drawerElement.classList.remove('is-expanded'); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         it("has Snapshots indicator", () => { | ||||
|             const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined; | ||||
|             expect(hasSnapshotIndicator).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("snapshots container has class isExpanded", () => { | ||||
|             let classes = drawerElement.classList; | ||||
|             const isExpandedBefore = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterFirstClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             const success = isExpandedBefore === false | ||||
|                 && isExpandedAfterFirstClick === true; | ||||
|  | ||||
|             expect(success).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("snapshots container does not have class isExpanded", () => { | ||||
|             let classes = drawerElement.classList; | ||||
|             const isExpandedBefore = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterFirstClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterSecondClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             const success = isExpandedBefore === false | ||||
|                 && isExpandedAfterFirstClick === true | ||||
|                 && isExpandedAfterSecondClick === false; | ||||
|  | ||||
|             expect(success).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("show notebook snapshots container text", () => { | ||||
|             clickSnapshotIndicator(); | ||||
|  | ||||
|             const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name'); | ||||
|             const snapshotsText = notebookSnapshots.textContent.trim(); | ||||
|  | ||||
|             expect(snapshotsText).toBe('Notebook Snapshots'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export default class Snapshot { | ||||
|             .then(domainObject => { | ||||
|                 addNotebookEntry(this.openmct, domainObject, notebookStorage, embed); | ||||
|  | ||||
|                 const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`; | ||||
|                 const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`; | ||||
|                 const msg = `Saved to Notebook ${defaultPath}`; | ||||
|                 this._showNotification(msg); | ||||
|             }); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
|  | ||||
| export const DEFAULT_CLASS = 'notebook-default'; | ||||
| const TIME_BOUNDS = { | ||||
|     START_BOUND: 'tc.startBound', | ||||
|     END_BOUND: 'tc.endBound', | ||||
| @@ -102,7 +103,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) { | ||||
| export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') { | ||||
|     if (!openmct || !domainObject || !notebookStorage) { | ||||
|         return; | ||||
|     } | ||||
| @@ -124,10 +125,11 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = | ||||
|     defaultEntries.push({ | ||||
|         id, | ||||
|         createdOn: date, | ||||
|         text: '', | ||||
|         text: entryText, | ||||
|         embeds | ||||
|     }); | ||||
|  | ||||
|     addDefaultClass(domainObject, openmct); | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', entries); | ||||
|  | ||||
|     return id; | ||||
| @@ -193,5 +195,10 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se | ||||
|     } | ||||
|  | ||||
|     delete entries[selectedSection.id][selectedPage.id]; | ||||
|  | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', entries); | ||||
| } | ||||
|  | ||||
| function addDefaultClass(domainObject, openmct) { | ||||
|     openmct.status.set(domainObject.identifier, DEFAULT_CLASS); | ||||
| } | ||||
|   | ||||
| @@ -60,7 +60,6 @@ export function setDefaultNotebookSection(section) { | ||||
|  | ||||
|     notebookStorage.section = section; | ||||
|     saveDefaultNotebook(notebookStorage); | ||||
|  | ||||
| } | ||||
|  | ||||
| export function setDefaultNotebookPage(page) { | ||||
|   | ||||
| @@ -86,7 +86,10 @@ export default class CouchObjectProvider { | ||||
|                 this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); | ||||
|             } | ||||
|  | ||||
|             this.objectQueue[key].updateRevision(response[REV]); | ||||
|             //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress | ||||
|             if (!this.objectQueue[key].pending) { | ||||
|                 this.objectQueue[key].updateRevision(response[REV]); | ||||
|             } | ||||
|  | ||||
|             return object; | ||||
|         } else { | ||||
|   | ||||
| @@ -22,9 +22,10 @@ | ||||
|  | ||||
| import CouchObjectProvider from './CouchObjectProvider'; | ||||
| const NAMESPACE = ''; | ||||
| const PERSISTENCE_SPACE = 'mct'; | ||||
|  | ||||
| export default function CouchPlugin(url) { | ||||
|     return function install(openmct) { | ||||
|         openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE)); | ||||
|         openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -31,19 +31,18 @@ describe('the plugin', () => { | ||||
|     let element; | ||||
|     let child; | ||||
|     let provider; | ||||
|     let testSpace = 'testSpace'; | ||||
|     let testPath = '/test/db'; | ||||
|     let mockDomainObject; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         mockDomainObject = { | ||||
|             identifier: { | ||||
|                 namespace: '', | ||||
|                 namespace: 'mct', | ||||
|                 key: 'some-value' | ||||
|             } | ||||
|         }; | ||||
|         openmct = createOpenMct(false); | ||||
|         openmct.install(new CouchPlugin(testSpace, testPath)); | ||||
|         openmct.install(new CouchPlugin(testPath)); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
|                 <div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div> | ||||
|                 <div class="plot-legend-item" | ||||
|                      ng-class="{ | ||||
|                         'is-missing': series.domainObject.status === 'missing' | ||||
|                         'is-status--missing': series.domainObject.status === 'missing' | ||||
|                     }" | ||||
|                      ng-repeat="series in series track by $index" | ||||
|                 > | ||||
| @@ -48,7 +48,7 @@ | ||||
|                         <span class="plot-series-color-swatch" | ||||
|                               ng-style="{ 'background-color': series.get('color').asHexString() }"> | ||||
|                         </span> | ||||
|                         <span class="is-missing__indicator" title="This item is missing"></span> | ||||
|                         <span class="is-status__indicator" title="This item is missing or suspect"></span> | ||||
|                         <span class="plot-series-name">{{ series.nameWithUnit() }}</span> | ||||
|                     </div> | ||||
|                     <div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}" | ||||
| @@ -95,14 +95,14 @@ | ||||
|                     <tr ng-repeat="series in series" | ||||
|                         class="plot-legend-item" | ||||
|                         ng-class="{ | ||||
|                             'is-missing': series.domainObject.status === 'missing' | ||||
|                             'is-status--missing': series.domainObject.status === 'missing' | ||||
|                         }" | ||||
|                     > | ||||
|                         <td class="plot-series-swatch-and-name"> | ||||
|                             <span class="plot-series-color-swatch" | ||||
|                                   ng-style="{ 'background-color': series.get('color').asHexString() }"> | ||||
|                             </span> | ||||
|                             <span class="is-missing__indicator" title="This item is missing"></span> | ||||
|                             <span class="is-status__indicator" title="This item is missing or suspect"></span> | ||||
|                             <span class="plot-series-name">{{ series.get('name') }}</span> | ||||
|                         </td> | ||||
|  | ||||
| @@ -188,15 +188,19 @@ | ||||
|                          ng-style="{ | ||||
|                              right: (100 * (max - tick.value) / interval) + '%', | ||||
|                              height: '100%' | ||||
|                          }"> | ||||
|                      </div> | ||||
|                          }" | ||||
|                          ng-show="plot.gridLines" | ||||
|                     > | ||||
|                     </div> | ||||
|                 </mct-ticks> | ||||
|  | ||||
|                 <mct-ticks axis="yAxis"> | ||||
|                      <div class="gl-plot-hash hash-h" | ||||
|                     <div class="gl-plot-hash hash-h" | ||||
|                           ng-repeat="tick in ticks track by tick.value" | ||||
|                           ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"> | ||||
|                      </div> | ||||
|                           ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }" | ||||
|                           ng-show="plot.gridLines" | ||||
|                     > | ||||
|                     </div> | ||||
|                 </mct-ticks> | ||||
|  | ||||
|                 <mct-chart config="config" | ||||
|   | ||||
| @@ -22,16 +22,16 @@ | ||||
| <div ng-controller="PlotController as controller" | ||||
|     class="c-plot holder holder-plot has-control-bar"> | ||||
|     <div class="c-control-bar" ng-show="!controller.hideExportButtons"> | ||||
|          <span class="c-button-set c-button-set--strip-h"> | ||||
|         <span class="c-button-set c-button-set--strip-h"> | ||||
|             <button class="c-button icon-download" | ||||
|                ng-click="controller.exportPNG()" | ||||
|                title="Export This View's Data as PNG"> | ||||
|                  <span class="c-button__label">PNG</span> | ||||
|                 ng-click="controller.exportPNG()" | ||||
|                 title="Export This View's Data as PNG"> | ||||
|                 <span class="c-button__label">PNG</span> | ||||
|             </button> | ||||
|             <button class="c-button" | ||||
|                ng-click="controller.exportJPG()" | ||||
|                title="Export This View's Data as JPG"> | ||||
|                  <span class="c-button__label">JPG</span> | ||||
|                 ng-click="controller.exportJPG()" | ||||
|                 title="Export This View's Data as JPG"> | ||||
|                 <span class="c-button__label">JPG</span> | ||||
|             </button> | ||||
|         </span> | ||||
|         <button class="c-button icon-crosshair" | ||||
| @@ -39,6 +39,11 @@ | ||||
|                 ng-click="controller.toggleCursorGuide($event)" | ||||
|                 title="Toggle cursor guides"> | ||||
|         </button> | ||||
|         <button class="c-button" | ||||
|                 ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }" | ||||
|                 ng-click="controller.toggleGridLines($event)" | ||||
|                 title="Toggle grid lines"> | ||||
|         </button> | ||||
|     </div> | ||||
|  | ||||
|     <div class="l-view-section"> | ||||
|   | ||||
| @@ -22,23 +22,28 @@ | ||||
| <div ng-controller="StackedPlotController as stackedPlot" | ||||
|       class="c-plot c-plot--stacked holder holder-plot has-control-bar"> | ||||
|     <div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons"> | ||||
|        <span class="c-button-set c-button-set--strip-h"> | ||||
|           <button class="c-button icon-download" | ||||
|              ng-click="stackedPlot.exportPNG()" | ||||
|              title="Export This View's Data as PNG"> | ||||
|               <span class="c-button__label">PNG</span> | ||||
|           </button> | ||||
|           <button class="c-button" | ||||
|              ng-click="stackedPlot.exportJPG()" | ||||
|              title="Export This View's Data as JPG"> | ||||
|               <span class="c-button__label">JPG</span> | ||||
|           </button> | ||||
|         <span class="c-button-set c-button-set--strip-h"> | ||||
|             <button class="c-button icon-download" | ||||
|                 ng-click="stackedPlot.exportPNG()" | ||||
|                 title="Export This View's Data as PNG"> | ||||
|                 <span class="c-button__label">PNG</span> | ||||
|             </button> | ||||
|             <button class="c-button" | ||||
|                 ng-click="stackedPlot.exportJPG()" | ||||
|                 title="Export This View's Data as JPG"> | ||||
|                 <span class="c-button__label">JPG</span> | ||||
|             </button> | ||||
|         </span> | ||||
|         <button class="c-button icon-crosshair" | ||||
|                 ng-class="{ 'is-active': stackedPlot.cursorGuide }" | ||||
|                 ng-click="stackedPlot.toggleCursorGuide($event)" | ||||
|                 title="Toggle cursor guides"> | ||||
|         </button> | ||||
|         <button class="c-button" | ||||
|                 ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }" | ||||
|                 ng-click="stackedPlot.toggleGridLines($event)" | ||||
|                 title="Toggle grid lines"> | ||||
|         </button> | ||||
|     </div> | ||||
|     <div class="l-view-section"> | ||||
|         <div class="c-loading--overlay loading" | ||||
|   | ||||
| @@ -96,7 +96,10 @@ define([ | ||||
|         this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h'); | ||||
|         this.cursorGuide = false; | ||||
|  | ||||
|         this.gridLines = true; | ||||
|  | ||||
|         this.listenTo(this.$scope, 'cursorguide', this.toggleCursorGuide, this); | ||||
|         this.listenTo(this.$scope, 'toggleGridLines', this.toggleGridLines, this); | ||||
|  | ||||
|         this.listenTo(this.$scope, '$destroy', this.destroy, this); | ||||
|         this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this); | ||||
| @@ -554,6 +557,10 @@ define([ | ||||
|         this.cursorGuide = !this.cursorGuide; | ||||
|     }; | ||||
|  | ||||
|     MCTPlotController.prototype.toggleGridLines = function ($event) { | ||||
|         this.gridLines = !this.gridLines; | ||||
|     }; | ||||
|  | ||||
|     MCTPlotController.prototype.getXKeyOption = function (key) { | ||||
|         return this.$scope.xKeyOptions.find(option => option.key === key); | ||||
|     }; | ||||
|   | ||||
| @@ -60,6 +60,7 @@ define([ | ||||
|         this.objectService = objectService; | ||||
|         this.exportImageService = exportImageService; | ||||
|         this.cursorGuide = false; | ||||
|         this.gridLines = true; | ||||
|  | ||||
|         $scope.pending = 0; | ||||
|  | ||||
| @@ -331,6 +332,11 @@ define([ | ||||
|         this.$scope.$broadcast('cursorguide', $event); | ||||
|     }; | ||||
|  | ||||
|     PlotController.prototype.toggleGridLines = function ($event) { | ||||
|         this.gridLines = !this.gridLines; | ||||
|         this.$scope.$broadcast('toggleGridLines', $event); | ||||
|     }; | ||||
|  | ||||
|     return PlotController; | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -160,5 +160,10 @@ define([], function () { | ||||
|         this.$scope.$broadcast('cursorguide', $event); | ||||
|     }; | ||||
|  | ||||
|     StackedPlotController.prototype.toggleGridLines = function ($event) { | ||||
|         this.gridLines = !this.gridLines; | ||||
|         this.$scope.$broadcast('toggleGridLines', $event); | ||||
|     }; | ||||
|  | ||||
|     return StackedPlotController; | ||||
| }); | ||||
|   | ||||
| @@ -57,7 +57,9 @@ define([ | ||||
|     './notificationIndicator/plugin', | ||||
|     './newFolderAction/plugin', | ||||
|     './persistence/couch/plugin', | ||||
|     './defaultRootName/plugin' | ||||
|     './defaultRootName/plugin', | ||||
|     './timeline/plugin', | ||||
|     './viewDatumAction/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
|     UTCTimeSystem, | ||||
| @@ -95,7 +97,9 @@ define([ | ||||
|     NotificationIndicator, | ||||
|     NewFolderAction, | ||||
|     CouchDBPlugin, | ||||
|     DefaultRootName | ||||
|     DefaultRootName, | ||||
|     Timeline, | ||||
|     ViewDatumAction | ||||
| ) { | ||||
|     const bundleMap = { | ||||
|         LocalStorage: 'platform/persistence/local', | ||||
| @@ -188,6 +192,8 @@ define([ | ||||
|     plugins.NewFolderAction = NewFolderAction.default; | ||||
|     plugins.ISOTimeFormat = ISOTimeFormat.default; | ||||
|     plugins.DefaultRootName = DefaultRootName.default; | ||||
|     plugins.Timeline = Timeline.default; | ||||
|     plugins.ViewDatumAction = ViewDatumAction.default; | ||||
|  | ||||
|     return plugins; | ||||
| }); | ||||
|   | ||||
| @@ -25,6 +25,8 @@ export default class RemoveAction { | ||||
|         this.key = 'remove'; | ||||
|         this.description = 'Remove this object from its containing object.'; | ||||
|         this.cssClass = "icon-trash"; | ||||
|         this.group = "action"; | ||||
|         this.priority = 1; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
| @@ -103,6 +105,16 @@ export default class RemoveAction { | ||||
|         let parentType = parent && this.openmct.types.get(parent.type); | ||||
|         let child = objectPath[0]; | ||||
|         let locked = child.locked ? child.locked : parent && parent.locked; | ||||
|         let isEditing = this.openmct.editor.isEditing(); | ||||
|  | ||||
|         if (isEditing) { | ||||
|             let currentItemInView = this.openmct.router.path[0]; | ||||
|             let domainObject = objectPath[0]; | ||||
|  | ||||
|             if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (locked) { | ||||
|             return false; | ||||
|   | ||||
| @@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction"; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.contextMenu.registerAction(new RemoveAction(openmct)); | ||||
|         openmct.actions.register(new RemoveAction(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -13,14 +13,7 @@ | ||||
|     } | ||||
|  | ||||
|     &__tab { | ||||
|         &:before { | ||||
|             margin-right: $interiorMarginSm; | ||||
|             opacity: 0.7; | ||||
|         } | ||||
|  | ||||
|         &__label { | ||||
|             flex: 1 1 auto; | ||||
|         } | ||||
|         justify-content: space-between; // Places remove button to far side of tab | ||||
|  | ||||
|         &__close-btn { | ||||
|             flex: 0 0 auto; | ||||
|   | ||||
| @@ -28,7 +28,18 @@ | ||||
|             }" | ||||
|             @click="showTab(tab, index)" | ||||
|         > | ||||
|             <span class="c-button__label c-tabs-view__tab__label">{{ tab.domainObject.name }}</span> | ||||
|             <div class="c-tabs-view__tab__label c-object-label" | ||||
|                  :class="[tab.status ? `is-${tab.status}` : '']" | ||||
|             > | ||||
|                 <div class="c-object-label__type-icon" | ||||
|                      :class="tab.type.definition.cssClass" | ||||
|                 > | ||||
|                     <span class="is-status__indicator" | ||||
|                           title="This item is missing or suspect" | ||||
|                     ></span> | ||||
|                 </div> | ||||
|                 <span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span> | ||||
|             </div> | ||||
|             <button v-if="isEditing" | ||||
|                     class="icon-x c-click-icon c-tabs-view__tab__close-btn" | ||||
|                     @click="showRemoveDialog(index)" | ||||
| @@ -181,8 +192,10 @@ export default { | ||||
|         }, | ||||
|         addItem(domainObject) { | ||||
|             let type = this.openmct.types.get(domainObject.type) || unknownObjectType; | ||||
|             let status = this.openmct.status.get(domainObject.identifier); | ||||
|             let tabItem = { | ||||
|                 domainObject, | ||||
|                 status, | ||||
|                 type: type, | ||||
|                 key: this.openmct.objects.makeKeyString(domainObject.identifier) | ||||
|             }; | ||||
|   | ||||
| @@ -160,7 +160,6 @@ define([ | ||||
|         processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) { | ||||
|             let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); | ||||
|             this.boundedRows.add(telemetryRows); | ||||
|             this.emit('historical-rows-processed'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -26,6 +26,7 @@ define([], function () { | ||||
|             this.columns = columns; | ||||
|  | ||||
|             this.datum = createNormalizedDatum(datum, columns); | ||||
|             this.fullDatum = datum; | ||||
|             this.limitEvaluator = limitEvaluator; | ||||
|             this.objectKeyString = objectKeyString; | ||||
|         } | ||||
| @@ -87,7 +88,7 @@ define([], function () { | ||||
|         } | ||||
|  | ||||
|         getContextMenuActions() { | ||||
|             return []; | ||||
|             return ['viewDatumAction']; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -54,15 +54,13 @@ define([ | ||||
|             view(domainObject, objectPath) { | ||||
|                 let table = new TelemetryTable(domainObject, openmct); | ||||
|                 let component; | ||||
|  | ||||
|                 let markingProp = { | ||||
|                     enable: true, | ||||
|                     useAlternateControlBar: false, | ||||
|                     rowName: '', | ||||
|                     rowNamePlural: '' | ||||
|                 }; | ||||
|  | ||||
|                 return { | ||||
|                 const view = { | ||||
|                     show: function (element, editMode) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
| @@ -72,7 +70,8 @@ define([ | ||||
|                             data() { | ||||
|                                 return { | ||||
|                                     isEditing: editMode, | ||||
|                                     markingProp | ||||
|                                     markingProp, | ||||
|                                     view | ||||
|                                 }; | ||||
|                             }, | ||||
|                             provide: { | ||||
| @@ -80,7 +79,7 @@ define([ | ||||
|                                 table, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             template: '<table-component :isEditing="isEditing" :marking="markingProp"/>' | ||||
|                             template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     onEditModeChange(editMode) { | ||||
| @@ -89,11 +88,22 @@ define([ | ||||
|                     onClearData() { | ||||
|                         table.clearData(); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.tableComponent.getViewContext(); | ||||
|                         } else { | ||||
|                             return { | ||||
|                                 type: 'telemetry-table' | ||||
|                             }; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function (element) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 return view; | ||||
|             }, | ||||
|             priority() { | ||||
|                 return 1; | ||||
|   | ||||
							
								
								
									
										123
									
								
								src/plugins/telemetryTable/ViewActions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/plugins/telemetryTable/ViewActions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| let exportCSV = { | ||||
|     name: 'Export Table Data', | ||||
|     key: 'export-csv-all', | ||||
|     description: "Export this view's data", | ||||
|     cssClass: 'icon-download labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().exportAllDataAsCSV(); | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| let exportMarkedRows = { | ||||
|     name: 'Export Marked Rows', | ||||
|     key: 'export-csv-marked', | ||||
|     description: "Export marked rows as CSV", | ||||
|     cssClass: 'icon-download labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().exportMarkedRows(); | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| let unmarkAllRows = { | ||||
|     name: 'Unmark All Rows', | ||||
|     key: 'unmark-all-rows', | ||||
|     description: 'Unmark all rows', | ||||
|     cssClass: 'icon-x labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().unmarkAllRows(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let pause = { | ||||
|     name: 'Pause', | ||||
|     key: 'pause-data', | ||||
|     description: 'Pause real-time data flow', | ||||
|     cssClass: 'icon-pause', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let play = { | ||||
|     name: 'Play', | ||||
|     key: 'play-data', | ||||
|     description: 'Continue real-time data flow', | ||||
|     cssClass: 'c-button pause-play is-paused', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().togglePauseByButton(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let expandColumns = { | ||||
|     name: 'Expand Columns', | ||||
|     key: 'expand-columns', | ||||
|     description: "Increase column widths to fit currently available data.", | ||||
|     cssClass: 'icon-arrows-right-left labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().expandColumns(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
| let autosizeColumns = { | ||||
|     name: 'Autosize Columns', | ||||
|     key: 'autosize-columns', | ||||
|     description: "Automatically size columns to fit the table into the available space.", | ||||
|     cssClass: 'icon-expand labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().autosizeColumns(); | ||||
|     }, | ||||
|     showInStatusBar: true, | ||||
|     group: 'view' | ||||
| }; | ||||
|  | ||||
| let viewActions = [ | ||||
|     exportCSV, | ||||
|     exportMarkedRows, | ||||
|     unmarkAllRows, | ||||
|     pause, | ||||
|     play, | ||||
|     expandColumns, | ||||
|     autosizeColumns | ||||
| ]; | ||||
|  | ||||
| viewActions.forEach(action => { | ||||
|     action.appliesTo = (objectPath, viewProvider = {}) => { | ||||
|         let viewContext = viewProvider.getViewContext && viewProvider.getViewContext(); | ||||
|  | ||||
|         if (viewContext) { | ||||
|             let type = viewContext.type; | ||||
|  | ||||
|             return type === 'telemetry-table'; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     }; | ||||
| }); | ||||
|  | ||||
| export default viewActions; | ||||
| @@ -0,0 +1,29 @@ | ||||
| .c-table-indicator { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-size: 0.9em; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     &__elem { | ||||
|         @include ellipsize(); | ||||
|         flex: 0 1 auto; | ||||
|         padding: 2px; | ||||
|         text-transform: uppercase; | ||||
|  | ||||
|         > * { | ||||
|             //display: contents; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__counts { | ||||
|         //background: rgba(deeppink, 0.1); | ||||
|         display: flex; | ||||
|         flex: 1 1 auto; | ||||
|         justify-content: flex-end; | ||||
|         overflow: hidden; | ||||
|  | ||||
|         > * { | ||||
|             margin-left: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +1,41 @@ | ||||
| <template> | ||||
| <div | ||||
|     v-if="filterNames.length > 0" | ||||
|     :title="title" | ||||
|     class="c-filter-indication" | ||||
|     :class="{ 'c-filter-indication--mixed': hasMixedFilters }" | ||||
|     class="c-table-indicator" | ||||
|     :class="{ 'is-filtering': filterNames.length > 0 }" | ||||
| > | ||||
|     <span class="c-filter-indication__mixed">{{ label }}</span> | ||||
|     <span | ||||
|         v-for="(name, index) in filterNames" | ||||
|         :key="index" | ||||
|         class="c-filter-indication__label" | ||||
|     <div | ||||
|         v-if="filterNames.length > 0" | ||||
|         class="c-table-indicator__filter c-table-indicator__elem c-filter-indication" | ||||
|         :class="{ 'c-filter-indication--mixed': hasMixedFilters }" | ||||
|         :title="title" | ||||
|     > | ||||
|         {{ name }} | ||||
|     </span> | ||||
|         <span class="c-filter-indication__mixed">{{ label }}</span> | ||||
|         <span | ||||
|             v-for="(name, index) in filterNames" | ||||
|             :key="index" | ||||
|             class="c-filter-indication__label" | ||||
|         > | ||||
|             {{ name }} | ||||
|         </span> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="c-table-indicator__counts"> | ||||
|         <span | ||||
|             :title="totalRows + ' rows visible after any filtering'" | ||||
|             class="c-table-indicator__elem c-table-indicator__row-count" | ||||
|         > | ||||
|             {{ totalRows }} Rows | ||||
|         </span> | ||||
| 
 | ||||
|         <span | ||||
|             v-if="markedRows" | ||||
|             class="c-table-indicator__elem c-table-indicator__marked-count" | ||||
|             :title="markedRows + ' rows selected'" | ||||
|         > | ||||
|             {{ markedRows }} Marked | ||||
|         </span> | ||||
| 
 | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| @@ -27,6 +50,16 @@ const USE_GLOBAL = 'useGlobal'; | ||||
| 
 | ||||
| export default { | ||||
|     inject: ['openmct', 'table'], | ||||
|     props: { | ||||
|         markedRows: { | ||||
|             type: Number, | ||||
|             default: 0 | ||||
|         }, | ||||
|         totalRows: { | ||||
|             type: Number, | ||||
|             default: 0 | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             filterNames: [], | ||||
| @@ -102,7 +102,17 @@ export default { | ||||
|                 selectable[columnKeys] = this.row.columns[columnKeys].selectable; | ||||
|  | ||||
|                 return selectable; | ||||
|             }, {}) | ||||
|             }, {}), | ||||
|             actionsViewContext: { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: this.getDatum, | ||||
|                         skipCache: true | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -170,14 +180,24 @@ export default { | ||||
|                 event.stopPropagation(); | ||||
|             } | ||||
|         }, | ||||
|         getDatum() { | ||||
|             return this.row.fullDatum; | ||||
|         }, | ||||
|         showContextMenu: function (event) { | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             this.markRow(event); | ||||
|  | ||||
|             this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => { | ||||
|                 let contextualObjectPath = this.objectPath.slice(); | ||||
|                 contextualObjectPath.unshift(domainObject); | ||||
|  | ||||
|                 this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions()); | ||||
|                 let allActions = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext); | ||||
|                 let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]); | ||||
|  | ||||
|                 if (applicableActions.length) { | ||||
|                     this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -96,10 +96,6 @@ | ||||
|         height: 0; // Fixes Chrome 73 overflow bug | ||||
|         overflow-x: auto; | ||||
|         overflow-y: scroll; | ||||
|  | ||||
|         .is-editing & { | ||||
|             pointer-events: none; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /******************************* TABLES */ | ||||
| @@ -114,6 +110,10 @@ | ||||
|             position: absolute; | ||||
|             height: 18px; // Needed when a row has empty values in its cells | ||||
|  | ||||
|             .is-editing .l-layout__frame & { | ||||
|                 pointer-events: none; | ||||
|             } | ||||
|  | ||||
|             &.is-selected { | ||||
|                 background-color: $colorSelectedBg !important; | ||||
|                 color: $colorSelectedFg !important; | ||||
| @@ -150,6 +150,35 @@ | ||||
|             white-space: nowrap; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__footer { | ||||
|         $pt: 2px; | ||||
|         border-top: 1px solid $colorInteriorBorder; | ||||
|         margin-top: $interiorMargin; | ||||
|         padding: $pt 0; | ||||
|         overflow: hidden; | ||||
|         transition: all 250ms; | ||||
|  | ||||
|         &:not(.is-filtering) { | ||||
|             .c-frame & { | ||||
|                 height: 0; | ||||
|                 padding: 0; | ||||
|                 visibility: hidden; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .c-frame & { | ||||
|         // target .c-frame .c-telemetry-table {} | ||||
|         $pt: 2px; | ||||
|         &:hover { | ||||
|             .c-telemetry-table__footer:not(.is-filtering) { | ||||
|                 height: $pt + 16px; | ||||
|                 padding: initial; | ||||
|                 visibility: visible; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /******************************* SPECIFIC CASE WRAPPERS */ | ||||
|   | ||||
| @@ -23,8 +23,7 @@ | ||||
| <div class="c-table-wrapper" | ||||
|      :class="{ 'is-paused': paused }" | ||||
| > | ||||
|     <!-- main contolbar  start--> | ||||
|     <div v-if="!marking.useAlternateControlBar" | ||||
|     <div v-if="enableLegacyToolbar" | ||||
|          class="c-table-control-bar c-control-bar" | ||||
|     > | ||||
|         <button | ||||
| @@ -94,7 +93,6 @@ | ||||
|  | ||||
|         <slot name="buttons"></slot> | ||||
|     </div> | ||||
|     <!-- main controlbar end --> | ||||
|  | ||||
|     <!-- alternate controlbar start --> | ||||
|     <div v-if="marking.useAlternateControlBar" | ||||
| @@ -113,11 +111,11 @@ | ||||
|  | ||||
|         <button | ||||
|             :class="{'hide-nice': !markedRows.length}" | ||||
|             class="c-button icon-x labeled" | ||||
|             class="c-icon-button icon-x labeled" | ||||
|             title="Deselect All" | ||||
|             @click="unmarkAllRows()" | ||||
|         > | ||||
|             <span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span> | ||||
|             <span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span> | ||||
|         </button> | ||||
|  | ||||
|         <slot name="buttons"></slot> | ||||
| @@ -253,7 +251,11 @@ | ||||
|                 :object-path="objectPath" | ||||
|             /> | ||||
|         </table> | ||||
|         <telemetry-filter-indicator /> | ||||
|         <table-footer-indicator | ||||
|             class="c-telemetry-table__footer" | ||||
|             :marked-rows="markedRows.length" | ||||
|             :total-rows="totalNumberOfRows" | ||||
|         /> | ||||
|     </div> | ||||
| </div><!-- closes c-table-wrapper --> | ||||
| </template> | ||||
| @@ -262,7 +264,7 @@ | ||||
| import TelemetryTableRow from './table-row.vue'; | ||||
| import search from '../../../ui/components/search.vue'; | ||||
| import TableColumnHeader from './table-column-header.vue'; | ||||
| import TelemetryFilterIndicator from './TelemetryFilterIndicator.vue'; | ||||
| import TableFooterIndicator from './table-footer-indicator.vue'; | ||||
| import CSVExporter from '../../../exporters/CSVExporter.js'; | ||||
| import _ from 'lodash'; | ||||
| import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; | ||||
| @@ -277,7 +279,7 @@ export default { | ||||
|         TelemetryTableRow, | ||||
|         TableColumnHeader, | ||||
|         search, | ||||
|         TelemetryFilterIndicator, | ||||
|         TableFooterIndicator, | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     inject: ['table', 'openmct', 'objectPath'], | ||||
| @@ -291,12 +293,12 @@ export default { | ||||
|             default: true | ||||
|         }, | ||||
|         allowFiltering: { | ||||
|             'type': Boolean, | ||||
|             'default': true | ||||
|             type: Boolean, | ||||
|             default: true | ||||
|         }, | ||||
|         allowSorting: { | ||||
|             'type': Boolean, | ||||
|             'default': true | ||||
|             type: Boolean, | ||||
|             default: true | ||||
|         }, | ||||
|         marking: { | ||||
|             type: Object, | ||||
| @@ -309,6 +311,17 @@ export default { | ||||
|                     rowNamePlural: "" | ||||
|                 }; | ||||
|             } | ||||
|         }, | ||||
|         enableLegacyToolbar: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         view: { | ||||
|             type: Object, | ||||
|             required: false, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -342,7 +355,8 @@ export default { | ||||
|             paused: false, | ||||
|             markedRows: [], | ||||
|             isShowingMarkedRowsOnly: false, | ||||
|             hideHeaders: configuration.hideHeaders | ||||
|             hideHeaders: configuration.hideHeaders, | ||||
|             totalNumberOfRows: 0 | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -383,6 +397,40 @@ export default { | ||||
|         markedRows: { | ||||
|             handler(newVal, oldVal) { | ||||
|                 this.$emit('marked-rows-updated', newVal, oldVal); | ||||
|  | ||||
|                 if (this.viewActionsCollection) { | ||||
|                     if (newVal.length > 0) { | ||||
|                         this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']); | ||||
|                     } else if (newVal.length === 0) { | ||||
|                         this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         paused: { | ||||
|             handler(newVal) { | ||||
|                 if (this.viewActionsCollection) { | ||||
|                     if (newVal) { | ||||
|                         this.viewActionsCollection.hide(['pause-data']); | ||||
|                         this.viewActionsCollection.show(['play-data']); | ||||
|                     } else { | ||||
|                         this.viewActionsCollection.hide(['play-data']); | ||||
|                         this.viewActionsCollection.show(['pause-data']); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         isAutosizeEnabled: { | ||||
|             handler(newVal) { | ||||
|                 if (this.viewActionsCollection) { | ||||
|                     if (newVal) { | ||||
|                         this.viewActionsCollection.show(['expand-columns']); | ||||
|                         this.viewActionsCollection.hide(['autosize-columns']); | ||||
|                     } else { | ||||
|                         this.viewActionsCollection.show(['autosize-columns']); | ||||
|                         this.viewActionsCollection.hide(['expand-columns']); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| @@ -395,6 +443,11 @@ export default { | ||||
|         this.rowsRemoved = _.throttle(this.rowsRemoved, 200); | ||||
|         this.scroll = _.throttle(this.scroll, 100); | ||||
|  | ||||
|         if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) { | ||||
|             this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view); | ||||
|             this.initializeViewActions(); | ||||
|         } | ||||
|  | ||||
|         this.table.on('object-added', this.addObject); | ||||
|         this.table.on('object-removed', this.removeObject); | ||||
|         this.table.on('outstanding-requests', this.outstandingRequests); | ||||
| @@ -451,6 +504,8 @@ export default { | ||||
|                     let filteredRows = this.table.filteredRows.getRows(); | ||||
|                     let filteredRowsLength = filteredRows.length; | ||||
|  | ||||
|                     this.totalNumberOfRows = filteredRowsLength; | ||||
|  | ||||
|                     if (filteredRowsLength < VISIBLE_ROW_COUNT) { | ||||
|                         end = filteredRowsLength; | ||||
|                     } else { | ||||
| @@ -833,7 +888,7 @@ export default { | ||||
|  | ||||
|                 for (let i = firstRowIndex; i <= lastRowIndex; i++) { | ||||
|                     let row = allRows[i]; | ||||
|                     row.marked = true; | ||||
|                     this.$set(row, 'marked', true); | ||||
|  | ||||
|                     if (row !== baseRow) { | ||||
|                         this.markedRows.push(row); | ||||
| @@ -894,6 +949,40 @@ export default { | ||||
|             this.isAutosizeEnabled = true; | ||||
|  | ||||
|             this.$nextTick().then(this.calculateColumnWidths); | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return { | ||||
|                 type: 'telemetry-table', | ||||
|                 exportAllDataAsCSV: this.exportAllDataAsCSV, | ||||
|                 exportMarkedRows: this.exportMarkedRows, | ||||
|                 unmarkAllRows: this.unmarkAllRows, | ||||
|                 togglePauseByButton: this.togglePauseByButton, | ||||
|                 expandColumns: this.recalculateColumnWidths, | ||||
|                 autosizeColumns: this.autosizeColumns | ||||
|             }; | ||||
|         }, | ||||
|         initializeViewActions() { | ||||
|             if (this.markedRows.length > 0) { | ||||
|                 this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']); | ||||
|             } else if (this.markedRows.length === 0) { | ||||
|                 this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']); | ||||
|             } | ||||
|  | ||||
|             if (this.paused) { | ||||
|                 this.viewActionsCollection.hide(['pause-data']); | ||||
|                 this.viewActionsCollection.show(['play-data']); | ||||
|             } else { | ||||
|                 this.viewActionsCollection.hide(['play-data']); | ||||
|                 this.viewActionsCollection.show(['pause-data']); | ||||
|             } | ||||
|  | ||||
|             if (this.isAutosizeEnabled) { | ||||
|                 this.viewActionsCollection.show(['expand-columns']); | ||||
|                 this.viewActionsCollection.hide(['autosize-columns']); | ||||
|             } else { | ||||
|                 this.viewActionsCollection.show(['autosize-columns']); | ||||
|                 this.viewActionsCollection.hide(['expand-columns']); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -1,37 +0,0 @@ | ||||
| .c-filter-indication { | ||||
|     @include userSelectNone(); | ||||
|     background: $colorFilterBg; | ||||
|     color: $colorFilterFg; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-size: 0.9em; | ||||
|     margin-top: $interiorMarginSm; | ||||
|     padding: 2px; | ||||
|     text-transform: uppercase; | ||||
|  | ||||
|     &:before { | ||||
|         font-family: symbolsfont-12px; | ||||
|         content: $glyph-icon-filter; | ||||
|         display: block; | ||||
|         font-size: 12px; | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     &__mixed { | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     &--mixed { | ||||
|         .c-filter-indication__mixed { | ||||
|             font-style: italic; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__label { | ||||
|         + .c-filter-indication__label { | ||||
|             &:before { | ||||
|                 content: ','; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -23,11 +23,13 @@ | ||||
| define([ | ||||
|     './TelemetryTableViewProvider', | ||||
|     './TableConfigurationViewProvider', | ||||
|     './TelemetryTableType' | ||||
|     './TelemetryTableType', | ||||
|     './ViewActions' | ||||
| ], function ( | ||||
|     TelemetryTableViewProvider, | ||||
|     TableConfigurationViewProvider, | ||||
|     TelemetryTableType | ||||
|     TelemetryTableType, | ||||
|     TelemetryTableViewActions | ||||
| ) { | ||||
|     return function plugin() { | ||||
|         return function install(openmct) { | ||||
| @@ -41,6 +43,10 @@ define([ | ||||
|                     return true; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             TelemetryTableViewActions.default.forEach(action => { | ||||
|                 openmct.actions.register(action); | ||||
|             }); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -168,6 +168,8 @@ describe("the plugin", () => { | ||||
|                 return telemetryPromise; | ||||
|             }); | ||||
|  | ||||
|             openmct.router.path = [testTelemetryObject]; | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(testTelemetryObject); | ||||
|             tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); | ||||
|             tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| @import "~styles/vendor/normalize-min"; | ||||
| @import "~styles/constants"; | ||||
| @import "~styles/constants-mobile.scss"; | ||||
| @import "../../styles/vendor/normalize-min"; | ||||
| @import "../../styles/constants"; | ||||
| @import "../../styles/constants-mobile.scss"; | ||||
|  | ||||
| @import "~styles/constants-espresso"; | ||||
| @import "../../styles/constants-espresso"; | ||||
|  | ||||
| @import "~styles/mixins"; | ||||
| @import "~styles/animations"; | ||||
| @import "~styles/about"; | ||||
| @import "~styles/glyphs"; | ||||
| @import "~styles/global"; | ||||
| @import "~styles/status"; | ||||
| @import "~styles/controls"; | ||||
| @import "~styles/forms"; | ||||
| @import "~styles/table"; | ||||
| @import "~styles/legacy"; | ||||
| @import "~styles/legacy-plots"; | ||||
| @import "~styles/plotly"; | ||||
| @import "~styles/legacy-messages"; | ||||
| @import "../../styles/mixins"; | ||||
| @import "../../styles/animations"; | ||||
| @import "../../styles/about"; | ||||
| @import "../../styles/glyphs"; | ||||
| @import "../../styles/global"; | ||||
| @import "../../styles/status"; | ||||
| @import "../../styles/controls"; | ||||
| @import "../../styles/forms"; | ||||
| @import "../../styles/table"; | ||||
| @import "../../styles/legacy"; | ||||
| @import "../../styles/legacy-plots"; | ||||
| @import "../../styles/plotly"; | ||||
| @import "../../styles/legacy-messages"; | ||||
|  | ||||
| @import "~styles/vue-styles.scss"; | ||||
| @import "../../styles/vue-styles.scss"; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user