Compare commits
	
		
			159 Commits
		
	
	
		
			static-roo
			...
			telemetry-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b939eb2fbe | ||
|   | 1afecdc82c | ||
|   | b9cda6985e | ||
|   | 401b7e3f19 | ||
|   | c9e9845172 | ||
|   | abbba38eac | ||
|   | db856251da | ||
|   | e1566e448d | ||
|   | d9aae0700c | ||
|   | 5c29726cc0 | ||
|   | 5c207c3fe0 | ||
|   | eba1dd1a4e | ||
|   | 570edc0dec | ||
|   | ad6bcd4ef8 | ||
|   | aedbbbbf75 | ||
|   | 3caaf00483 | ||
|   | 971eda4d88 | ||
|   | 090d216517 | ||
|   | d42d7ae68d | ||
|   | 68e6e3c121 | ||
|   | 7e7f39db2d | ||
|   | b6e0fca828 | ||
|   | ffc5896e5a | ||
|   | fd6ebd152f | ||
|   | 7a5c1c0e1f | ||
|   | 2f7e1e3f1a | ||
|   | d73746b51b | ||
|   | 2df54af019 | ||
|   | 586269f761 | ||
|   | e536ab34d7 | ||
|   | e15002dd72 | ||
|   | 453cf3ad6a | ||
|   | 5c46e48bde | ||
|   | 868ea9362f | ||
|   | d69106ff2c | ||
|   | 1658b17c56 | ||
|   | 39cf0528ca | ||
|   | 3d12f7312b | ||
|   | 22481fdc31 | ||
|   | 4a9d27dc79 | ||
|   | a5a9fefd40 | ||
|   | dae4074934 | ||
|   | a540a3573f | ||
|   | 4e7fe9082c | ||
|   | 568141bf81 | ||
|   | ac3ea43fe5 | ||
|   | e922e8d504 | ||
|   | 650a877d2a | ||
|   | 1202109c59 | ||
|   | 429d7bbd57 | ||
|   | af749fe71b | ||
|   | cf64c512ce | ||
|   | 14592d1c3e | ||
|   | c5bd3da44a | ||
|   | ff3e49e926 | ||
|   | e244a3e431 | ||
|   | c5d9fb6fd9 | ||
|   | 4c276ab422 | ||
|   | bf321abae4 | ||
|   | 7336968ef9 | ||
|   | d60956948b | ||
|   | 23d5c2e1ee | ||
|   | 2632b8891a | ||
|   | fff4cd9d51 | ||
|   | 7f9fd5c705 | ||
|   | be0291cf70 | ||
|   | b3a6d7271d | ||
|   | ebeed2f236 | ||
|   | 337c26c019 | ||
|   | 730f363f94 | ||
|   | ae30e6110b | ||
|   | 6e4bf3e45b | ||
|   | 5465ca92f9 | ||
|   | e55ea41b0a | ||
|   | 8cfb3cc689 | ||
|   | 2d728a1362 | ||
|   | 99333988df | ||
|   | de783d4286 | ||
|   | 1e1a2443d5 | ||
|   | d65e1e604e | ||
|   | f6c1488ccd | ||
|   | 26be1ecf37 | ||
|   | 38f0f072bb | ||
|   | e5e969665f | ||
|   | ffbb662c99 | ||
|   | bd7b23f896 | ||
|   | c238def902 | ||
|   | 2d430ece7f | ||
|   | c92644a661 | ||
|   | 41ce3c04f7 | ||
|   | fcf77f359f | ||
|   | 40a2737915 | ||
|   | 216489d67f | ||
|   | 418a393b26 | ||
|   | 1f3d744494 | ||
|   | ff3f2dccba | ||
|   | e69973bd29 | ||
|   | 05b352cc36 | ||
|   | 9735548999 | ||
|   | f9529b1362 | ||
|   | c598cec702 | ||
|   | e9ea1c4a0f | ||
|   | e9238ff282 | ||
|   | 4cebd72cba | ||
|   | f8a44d6e71 | ||
|   | d0745b300b | ||
|   | 2a4e0a3081 | ||
|   | 1ff19f9574 | ||
|   | 7ef84cb50d | ||
|   | cd05c70d64 | ||
|   | 568473b82f | ||
|   | c61b074755 | ||
|   | 8ed66ab4ab | ||
|   | b2502dd998 | ||
|   | 856eedbf9d | ||
|   | 0c0ca6e6af | ||
|   | 498b797e49 | ||
|   | 02c33388ba | ||
|   | 8a8e3cc055 | ||
|   | 36d60b16e9 | ||
|   | de3114568b | ||
|   | eb5835faeb | ||
|   | ff1ddb0b79 | ||
|   | 15b127bb2e | ||
|   | e4ed881f6d | ||
|   | 7b62cf130c | ||
|   | 72fd2e531c | ||
|   | 4a5392ef78 | ||
|   | 0150a708ca | ||
|   | eacc181d5e | ||
|   | 405bb55881 | ||
|   | 4a35508459 | ||
|   | 98a9d71a2e | ||
|   | a1596d0b06 | ||
|   | 4b3be4c483 | ||
|   | 0fa8472db1 | ||
|   | e1e2dca1d8 | ||
|   | 755c013ec8 | ||
|   | eab702b763 | ||
|   | d15446ac91 | ||
|   | 500733afb2 | ||
|   | 2aa04b0a56 | ||
|   | c051f342af | ||
|   | 8aeb365f5f | ||
|   | 827a28313d | ||
|   | c83de8aad2 | ||
|   | b55f43b8df | ||
|   | 8466723a90 | ||
|   | a103b4dbff | ||
|   | 826ac3a947 | ||
|   | 597327f138 | ||
|   | bef79402ca | ||
|   | e68e0c381f | ||
|   | c73f7259c2 | ||
|   | 4c9235ba10 | ||
|   | 55e2a77df8 | ||
|   | cfbff02e7f | ||
|   | 54980fb296 | ||
|   | ba98d9315c | 
| @@ -18,7 +18,7 @@ | ||||
|     "node-uuid": "^1.4.7", | ||||
|     "comma-separated-values": "^3.6.4", | ||||
|     "FileSaver.js": "^0.0.2", | ||||
|     "zepto": "^1.1.6", | ||||
|     "zepto": "1.2.0", | ||||
|     "eventemitter3": "^1.2.0", | ||||
|     "lodash": "3.10.1", | ||||
|     "almond": "~0.3.2", | ||||
|   | ||||
| @@ -59,7 +59,7 @@ define([ | ||||
|             if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) { | ||||
|                 workerRequest[prop] = domainObject.telemetry[prop]; | ||||
|             } | ||||
|             if (request.hasOwnProperty(prop)) { | ||||
|             if (request && request.hasOwnProperty(prop)) { | ||||
|                 workerRequest[prop] = request[prop]; | ||||
|             } | ||||
|             if (!workerRequest[prop]) { | ||||
|   | ||||
| @@ -121,7 +121,7 @@ | ||||
|         <h2>Palettes</h2> | ||||
|         <div class="cols cols1-1"> | ||||
|             <div class="col"> | ||||
|                 <p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one.</p> | ||||
|                 <p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one. Selected palette choices should utilize the <code>selected</code> CSS class to visualize indicate that state.</p> | ||||
|                 <p>Note that while this example uses static markup for illustrative purposes, don't do this - use a front-end framework with repeaters to build the color choices.</p> | ||||
|             </div> | ||||
|             <mct-example><div style="height: 220px" title="Ignore me, I'm just here to provide space for this example."> | ||||
| @@ -129,9 +129,9 @@ | ||||
| <div class="s-button s-menu-button menu-element t-color-palette icon-paint-bucket" ng-controller="ClickAwayController as toggle"> | ||||
|     <span class="l-click-area" ng-click="toggle.toggle()"></span> | ||||
|     <span class="color-swatch" style="background: rgb(255, 0, 0);"></span> | ||||
|     <div class="menu l-color-palette" ng-show="toggle.isActive()"> | ||||
|     <div class="menu l-palette l-color-palette" ng-show="toggle.isActive()"> | ||||
|         <div class="l-palette-row l-option-row"> | ||||
|             <div class="l-palette-item s-palette-item " ng-click="ngModel[field] = 'transparent'"></div> | ||||
|             <div class="l-palette-item s-palette-item no-selection"></div> | ||||
|             <span class="l-palette-item-label">None</span> | ||||
|         </div> | ||||
|         <div class="l-palette-row"> | ||||
| @@ -147,7 +147,7 @@ | ||||
|             <div class="l-palette-item s-palette-item" style="background: rgb(255, 255, 255);"></div> | ||||
|         </div> | ||||
|         <div class="l-palette-row"> | ||||
|             <div class="l-palette-item s-palette-item" style="background: rgb(136, 32, 32);"></div> | ||||
|             <div class="l-palette-item s-palette-item selected" style="background: rgb(255, 0, 0);"></div> | ||||
|             <div class="l-palette-item s-palette-item" style="background: rgb(224, 64, 64);"></div> | ||||
|             <div class="l-palette-item s-palette-item" style="background: rgb(240, 160, 72);"></div> | ||||
|             <div class="l-palette-item s-palette-item" style="background: rgb(255, 248, 96);"></div> | ||||
|   | ||||
| @@ -25,8 +25,7 @@ | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
|     <title></title> | ||||
|     <script src="bower_components/requirejs/require.js"> | ||||
|     </script> | ||||
|     <script src="bower_components/requirejs/require.js"> </script> | ||||
|     <script> | ||||
|         var THIRTY_MINUTES = 30 * 60 * 1000; | ||||
|  | ||||
| @@ -44,13 +43,14 @@ | ||||
|             openmct.install(openmct.plugins.ExampleImagery()); | ||||
|             openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|             openmct.install(openmct.plugins.ImportExport()); | ||||
|             openmct.install(openmct.plugins.TelemetryMean()); | ||||
|             openmct.install(openmct.plugins.Conductor({ | ||||
|                 menuOptions: [ | ||||
|                     { | ||||
|                         name: "Fixed", | ||||
|                         timeSystem: 'utc', | ||||
|                         bounds: { | ||||
|                             start: Date.now() - 30 * 60 * 1000, | ||||
|                             start: Date.now() - THIRTY_MINUTES, | ||||
|                             end: Date.now() | ||||
|                         } | ||||
|                     }, | ||||
| @@ -65,6 +65,7 @@ | ||||
|                     } | ||||
|                 ] | ||||
|             })); | ||||
|             openmct.install(openmct.plugins.SummaryWidget()); | ||||
|             openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0}); | ||||
|             openmct.time.timeSystem('utc'); | ||||
|             openmct.start(); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="abs top-bar"> | ||||
|     <div class="title">{{ngModel.title}}</div> | ||||
|     <div class="dialog-title">{{ngModel.title}}</div> | ||||
|     <div class="hint">All fields marked <span class="req icon-asterisk"></span> are required.</div> | ||||
| </div> | ||||
| <div class='abs editor'> | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| <div class="l-message" | ||||
|      ng-class="'message-severity-' + ngModel.severity"> | ||||
|     <div class="ui-symbol type-icon message-type"></div> | ||||
|     <div class="message-contents"> | ||||
|     <div class="w-message-contents"> | ||||
|         <div class="top-bar"> | ||||
|             <div class="title">{{ngModel.title}}</div> | ||||
|             <div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div> | ||||
|         </div> | ||||
|         <div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div> | ||||
|         <div class="message-body"> | ||||
|             <div class="message-action"> | ||||
|                 {{ngModel.actionText}} | ||||
| @@ -25,8 +24,6 @@ | ||||
|                ng-click="ngModel.primaryOption.callback()"> | ||||
|                 {{ngModel.primaryOption.label}} | ||||
|             </a> | ||||
|  | ||||
|         </div> | ||||
|  | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| <mct-container key="overlay" class="t-message-list"> | ||||
|     <div class="message-contents"> | ||||
|         <div class="abs top-bar"> | ||||
|             <div class="title">{{ngModel.dialog.title}}</div> | ||||
| <mct-container key="overlay"> | ||||
|     <div class="t-message-list"> | ||||
|         <div class="top-bar"> | ||||
|             <div class="dialog-title">{{ngModel.dialog.title}}</div> | ||||
|             <div class="hint">Displaying {{ngModel.dialog.messages.length}} message<span ng-show="ngModel.dialog.messages.length > 1 || | ||||
|                                                                                                   ngModel.dialog.messages.length == 0">s</span> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="abs message-body"> | ||||
|         <div class="w-messages"> | ||||
|             <mct-include | ||||
|                     ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'" | ||||
|                     key="'message'" ng-model="msg.model"></mct-include> | ||||
|                 ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'" | ||||
|                 key="'message'" ng-model="msg.model"></mct-include> | ||||
|         </div> | ||||
|         <div class="abs bottom-bar"> | ||||
|         <div class="bottom-bar"> | ||||
|             <a ng-repeat="dialogAction in ngModel.dialog.actions" | ||||
|                class="s-button major" | ||||
|                ng-click="dialogAction.action()"> | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
| --> | ||||
| <mct-container key="overlay"> | ||||
|     <div class="abs top-bar"> | ||||
|         <div class="title">{{ngModel.dialog.title}}</div> | ||||
|         <div class="dialog-title">{{ngModel.dialog.title}}</div> | ||||
|         <div class="hint">{{ngModel.dialog.hint}}</div> | ||||
|     </div> | ||||
|     <div class='abs editor'> | ||||
|   | ||||
| @@ -80,6 +80,12 @@ define( | ||||
|                 return closeEditor(); | ||||
|             } | ||||
|  | ||||
|             function resolveWith (object) { | ||||
|                 return function () { | ||||
|                     return object; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             newModel.type = this.type.getKey(); | ||||
|             newModel.location = this.parent.getId(); | ||||
|             newObject = this.parent.useCapability('instantiation', newModel); | ||||
|   | ||||
| @@ -137,6 +137,11 @@ | ||||
|         min-height: 0; | ||||
|         &.holder:not(:last-child) { margin-bottom: $interiorMarginLg; } | ||||
|     } | ||||
|     &.l-flex-accordion .flex-accordion-holder { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         //overflow: hidden !important; | ||||
|     } | ||||
|     .flex-container { @include flex-direction(column); } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -180,6 +180,20 @@ a.disabled { | ||||
|     @include ellipsize(); | ||||
| } | ||||
|  | ||||
| .no-selection { | ||||
|     // aka selection = "None". Used in palettes and their menu buttons. | ||||
|     $c: red; $s: 48%; $e: 52%; | ||||
|     @include background-image(linear-gradient(-45deg, | ||||
|         transparent $s - 5%, | ||||
|         $c $s, | ||||
|         $c $e, | ||||
|         transparent $e + 5% | ||||
|     )); | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: contain; | ||||
| } | ||||
|  | ||||
|  | ||||
| .scrolling, | ||||
| .scroll { | ||||
|     overflow: auto; | ||||
|   | ||||
| @@ -26,5 +26,6 @@ | ||||
|         display: block; | ||||
|         height: 100%; | ||||
|         width: 100%; | ||||
|         border: none; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
| /********************************* CONTROLS */ | ||||
| @import "controls/breadcrumb"; | ||||
| @import "controls/buttons"; | ||||
| @import "controls/color-palette"; | ||||
| @import "controls/palette"; | ||||
| @import "controls/controls"; | ||||
| @import "controls/lists"; | ||||
| @import "controls/menus"; | ||||
| @@ -80,3 +80,4 @@ | ||||
| @import "autoflow"; | ||||
| @import "features/imagery"; | ||||
| @import "features/time-display"; | ||||
| @import "widgets"; | ||||
|   | ||||
| @@ -50,7 +50,6 @@ | ||||
|         content:''; | ||||
|         font-family: symbolsfont; | ||||
|         font-size: 0.8em; | ||||
|         display: inline; | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										301
									
								
								platform/commonUI/general/res/sass/_widgets.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								platform/commonUI/general/res/sass/_widgets.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,301 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /************************************************************* WIDGET OBJECT */ | ||||
| .l-summary-widget { | ||||
|     // Widget layout classes here | ||||
|     @include ellipsize(); | ||||
|     display: inline-block; | ||||
|     text-align: center; | ||||
|     .widget-label:before { | ||||
|         // Widget icon | ||||
|         font-size: 0.9em; | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .s-summary-widget { | ||||
|     // Widget style classes here | ||||
|     @include boxShdw($shdwBtns); | ||||
|     border-radius: $basicCr; | ||||
|     border-style: solid; | ||||
|     border-width: 1px; | ||||
|     box-sizing: border-box; | ||||
|     cursor: default; | ||||
|     font-size: 0.8rem; | ||||
|     padding: $interiorMarginLg $interiorMarginLg * 2; | ||||
|     &[href] { | ||||
|         cursor: pointer; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .widget-edit-holder { | ||||
|     // Hide edit area when in browse mode | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .widget-rule-header { | ||||
|     @extend .l-flex-row; | ||||
|     @include align-items(center); | ||||
|     margin-bottom: $interiorMargin; | ||||
|     > .flex-elem { | ||||
|         &:not(:first-child) { | ||||
|             margin-left: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .widget-rules-wrapper, | ||||
| .widget-rule-content, | ||||
| .w-widget-test-data-content { | ||||
|     @include trans-prop-nice($props: (height, min-height, opacity), $dur: 250ms); | ||||
|     min-height: 0; | ||||
|     height: 0; | ||||
|     opacity: 0; | ||||
|     overflow: hidden; | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
| .widget-rules-wrapper { | ||||
|     flex: 1 1 auto !important; | ||||
| } | ||||
|  | ||||
| .widget-rule-content.expanded { | ||||
|     overflow: visible !important; | ||||
|     min-height: 50px; | ||||
|     height: auto; | ||||
|     opacity: 1; | ||||
|     pointer-events: inherit; | ||||
| } | ||||
|  | ||||
| .w-widget-test-data-content { | ||||
|     .l-enable { | ||||
|         padding: $interiorMargin 0; | ||||
|     } | ||||
|  | ||||
|     .w-widget-test-data-items { | ||||
|         max-height: 20vh; | ||||
|         overflow-y: scroll !important; | ||||
|         padding-right: $interiorMargin; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .l-widget-thumb-wrapper, | ||||
| .l-compact-form label { | ||||
|     $ruleLabelW: 40%; | ||||
|     $ruleLabelMaxW: 150px; | ||||
|     @include display(flex); | ||||
|     max-width: $ruleLabelMaxW; | ||||
|     width: $ruleLabelW; | ||||
| } | ||||
|  | ||||
| .t-message-widget-no-data { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| /********************************************************** EDITING A WIDGET */ | ||||
| .s-status-editing > mct-view > .w-summary-widget { | ||||
|     // Classes for editor layout while editing a widget | ||||
|     // This selector is ugly and brittle, but needed to prevent interface from showing when widget is in a layout | ||||
|     // being edited. | ||||
|     @include absPosDefault(); | ||||
|     @extend .l-flex-col; | ||||
|  | ||||
|     > .l-summary-widget { | ||||
|         // Main view of the summary widget | ||||
|         // Give some airspace and center the widget in the area | ||||
|         margin: 30px auto; | ||||
|     } | ||||
|  | ||||
|     .widget-edit-holder { | ||||
|         display: flex; // Overrides `display: none` during Browse mode | ||||
|         .flex-accordion-holder { | ||||
|             // Needed because otherwise accordion elements "creep" when contents expand and contract | ||||
|             display: block !important; | ||||
|         } | ||||
|         &.expanded-widget-test-data { | ||||
|             .w-widget-test-data-content { | ||||
|                 min-height: 50px; | ||||
|                 height: auto; | ||||
|                 opacity: 1; | ||||
|                 pointer-events: inherit; | ||||
|             } | ||||
|             &:not(.expanded-widget-rules) { | ||||
|                 // Test data is expanded and rules are collapsed | ||||
|                 // Make text data take up all the vertical space | ||||
|                 .flex-accordion-holder { display: flex; } | ||||
|                 .widget-test-data { | ||||
|                     flex-grow: 999999; | ||||
|                 } | ||||
|                 .w-widget-test-data-items { | ||||
|                     max-height: inherit; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         &.expanded-widget-rules { | ||||
|             .widget-rules-wrapper { | ||||
|                 min-height: 50px; | ||||
|                 height: auto; | ||||
|                 opacity: 1; | ||||
|                 pointer-events: inherit; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.s-status-no-data { | ||||
|         .widget-edit-holder { | ||||
|             opacity: 0.3; | ||||
|             pointer-events: none; | ||||
|         } | ||||
|         .t-message-widget-no-data { | ||||
|             display: flex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     .l-compact-form { | ||||
|         // Overrides on .l-compact-form | ||||
|         ul { | ||||
|             &:last-child { margin: 0; } | ||||
|             li { | ||||
|                 @include align-items(flex-start); | ||||
|                 @include flex-wrap(nowrap); | ||||
|                 line-height: 230%; // Provide enough space when controls wrap | ||||
|                 padding: 2px 0; | ||||
|                 &:not(.widget-rule-header) { | ||||
|                     &:not(.connects-to-previous) { | ||||
|                         border-top: 1px solid $colorFormLines; | ||||
|                     } | ||||
|                 } | ||||
|                 &.connects-to-previous { | ||||
|                     padding: $interiorMargin 0; | ||||
|                 } | ||||
|  | ||||
|                 > label { | ||||
|                     display: block; // Needed to align text to right | ||||
|                     text-align: right; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.s-widget-test-data-item { | ||||
|             // Single line of ul li label span, etc. | ||||
|             ul { | ||||
|                 li { | ||||
|                     border: none !important; | ||||
|                     > label { | ||||
|                         display: inline-block; | ||||
|                         width: auto; | ||||
|                         text-align: left; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .widget-edit-holder { | ||||
|     font-size: 0.8rem; | ||||
| } | ||||
|  | ||||
| .widget-rules-wrapper { | ||||
|     // Wrapper area that holds n rules | ||||
|     box-sizing: border-box; | ||||
|     overflow-y: scroll; | ||||
|     padding-right: $interiorMargin; | ||||
| } | ||||
|  | ||||
| .l-widget-rule, | ||||
| .l-widget-test-data-item { | ||||
|     box-sizing: border-box; | ||||
|     margin-bottom: $interiorMarginSm; | ||||
|     padding: $interiorMargin $interiorMarginLg; | ||||
| } | ||||
|  | ||||
| .l-widget-thumb-wrapper { | ||||
|     @extend .l-flex-row; | ||||
|     @include align-items(center); | ||||
|     > span { display: block; } | ||||
|     .grippy-holder, | ||||
|     .view-control { | ||||
|         margin-right: $interiorMargin; | ||||
|         width: 1em; | ||||
|         height: 1em; | ||||
|     } | ||||
|  | ||||
|     .widget-thumb { | ||||
|         @include flex(1 1 auto); | ||||
|         width: 100%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .rule-title { | ||||
|     @include flex(0 1 auto); | ||||
|     color: pullForward($colorBodyFg, 50%); | ||||
| } | ||||
|  | ||||
| .rule-description { | ||||
|     @include flex(1 1 auto); | ||||
|     @include ellipsize(); | ||||
|     color: pushBack($colorBodyFg, 20%); | ||||
| } | ||||
|  | ||||
| .s-widget-rule, | ||||
| .s-widget-test-data-item { | ||||
|     background-color: rgba($colorBodyFg, 0.1); | ||||
|     border-radius: $basicCr; | ||||
| } | ||||
|  | ||||
| .widget-thumb { | ||||
|     @include ellipsize(); | ||||
|     @extend .s-summary-widget; | ||||
|     @extend .l-summary-widget; | ||||
|     padding: $interiorMarginSm $interiorMargin; | ||||
| } | ||||
|  | ||||
| // Hide and show elements in the rule-header on hover | ||||
| .l-widget-rule, | ||||
| .l-widget-test-data-item { | ||||
|     .grippy, | ||||
|     .l-rule-action-buttons-wrapper, | ||||
|     .l-condition-action-buttons-wrapper, | ||||
|     .l-widget-test-data-item-action-buttons-wrapper { | ||||
|         @include trans-prop-nice($props: opacity, $dur: 500ms); | ||||
|         opacity: 0; | ||||
|     } | ||||
|     &:hover { | ||||
|         .grippy, | ||||
|         .l-rule-action-buttons-wrapper, | ||||
|         .l-widget-test-data-item-action-buttons-wrapper { | ||||
|             @include trans-prop-nice($props: opacity, $dur: 0); | ||||
|             opacity: 1; | ||||
|         } | ||||
|     } | ||||
|     .t-condition { | ||||
|         &:hover { | ||||
|             .l-condition-action-buttons-wrapper { | ||||
|                 @include trans-prop-nice($props: opacity, $dur: 0); | ||||
|                 opacity: 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -261,7 +261,7 @@ input[type="number"] { | ||||
| input[type="text"].lg {  width: 100% !important; } | ||||
| .l-input-med input[type="text"], | ||||
| input[type="text"].med { width: 200px !important; } | ||||
| input[type="text"].sm {  width: 50px !important; } | ||||
| input[type="text"].sm, input[type="number"].sm {  width: 50px !important; } | ||||
| .l-numeric input[type="text"], | ||||
| input[type="text"].numeric { text-align: right; } | ||||
|  | ||||
| @@ -317,14 +317,10 @@ input[type="text"].s-input-inline, | ||||
| .select { | ||||
|     @include btnSubtle($bg: $colorSelectBg); | ||||
|     @extend .icon-arrow-down; // Context arrow | ||||
|     @if $shdwBtns != none { | ||||
|         margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers | ||||
|     } | ||||
|     display: inline-block; | ||||
|     padding: 0 $interiorMargin; | ||||
|     overflow: hidden; | ||||
|     position: relative; | ||||
|     line-height: $formInputH; | ||||
|     select { | ||||
|         @include appearance(none); | ||||
|         box-sizing: border-box; | ||||
| @@ -340,11 +336,13 @@ input[type="text"].s-input-inline, | ||||
|         } | ||||
|     } | ||||
|     &:before { | ||||
|         pointer-events: none; | ||||
|         @include transform(translateY(-50%)); | ||||
|         color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent)); | ||||
|         display: block; | ||||
|         pointer-events: none; | ||||
|         position: absolute; | ||||
|         right: $interiorMargin; top: 0; | ||||
|         right: $interiorMargin; | ||||
|         top: 50%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -396,8 +394,7 @@ input[type="text"].s-input-inline, | ||||
|     .l-elem-wrapper { | ||||
|         mct-representation { | ||||
|             // Holds the context-available item | ||||
|             // Must have min-width to make flex work properly | ||||
|             // in Safari | ||||
|             // Must have min-width to make flex work properly in Safari | ||||
|             min-width: 0.7em; | ||||
|         } | ||||
|     } | ||||
| @@ -563,7 +560,6 @@ input[type="text"].s-input-inline, | ||||
|     height: $h; | ||||
|     margin-top: 1 + floor($h/2) * -1; | ||||
|     @include btnSubtle(pullForward($colorBtnBg, 10%)); | ||||
|     //border-radius: 50% !important; | ||||
| } | ||||
|  | ||||
| @mixin sliderKnobRound() { | ||||
| @@ -578,7 +574,6 @@ input[type="text"].s-input-inline, | ||||
|  | ||||
| input[type="range"] { | ||||
|     // HTML5 range inputs | ||||
|  | ||||
|     -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ | ||||
|     background: transparent; /* Otherwise white in Chrome */ | ||||
|     &:focus { | ||||
| @@ -736,6 +731,30 @@ textarea { | ||||
|     } | ||||
| } | ||||
|  | ||||
| .view-switcher, | ||||
| .t-btn-view-large { | ||||
|     @include trans-prop-nice-fade($controlFadeMs); | ||||
| } | ||||
|  | ||||
| .view-control { | ||||
|     @extend .icon-arrow-right; | ||||
|     cursor: pointer; | ||||
|     font-size: 0.75em; | ||||
|     &:before { | ||||
|         position: absolute; | ||||
|         @include trans-prop-nice(transform, 100ms); | ||||
|         @include transform-origin(center); | ||||
|     } | ||||
|     &.expanded:before { | ||||
|         @include transform(rotate(90deg)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .grippy { | ||||
|     @extend .icon-grippy; | ||||
|     cursor: move; | ||||
| } | ||||
|  | ||||
| /******************************************************** BROWSER ELEMENTS */ | ||||
| body.desktop { | ||||
|     ::-webkit-scrollbar { | ||||
|   | ||||
| @@ -29,23 +29,27 @@ | ||||
|     } | ||||
|  | ||||
|     .icon { | ||||
|         font-size: 16px; //120%; | ||||
|         font-size: 16px; | ||||
|     } | ||||
|  | ||||
|     .title-label { | ||||
|         margin-left: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     .icon-swatch, | ||||
|     .color-swatch { | ||||
|         // Used in color menu buttons in toolbar | ||||
|         $d: 10px; | ||||
|         display: inline-block; | ||||
|         border: 1px solid rgba($colorBtnFg, 0.2); | ||||
|         height: $d; | ||||
|         width: $d; | ||||
|         height: $d; width: $d; | ||||
|         line-height: $d; | ||||
|         vertical-align: middle; | ||||
|         margin-left: $interiorMarginSm; | ||||
|         margin-top: -2px; | ||||
|         &:not(.no-selection) { | ||||
|             border-color: transparent; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /******************************************************************* STATUS BLOCK ELEMS */ | ||||
| @mixin statusBannerColors($bg, $fg: $colorStatusFg) { | ||||
| 	$bgPb: 30%; | ||||
| 	$bgPbD: 10%; | ||||
| @@ -120,7 +120,11 @@ | ||||
| 	} | ||||
|  | ||||
| 	.status-indicator { | ||||
|         background: none !important; | ||||
| 		margin-right: $interiorMarginSm; | ||||
|         &[class*='s-status']:before { | ||||
|             font-size: 1em; | ||||
|         } | ||||
| 	} | ||||
|  | ||||
| 	.count { | ||||
| @@ -136,7 +140,7 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Styles for messages and message banners */ | ||||
| /******************************************************************* MESSAGE BANNERS */ | ||||
| .message { | ||||
| 	&.block { | ||||
| 		border-radius: $basicCr; | ||||
| @@ -192,7 +196,6 @@ | ||||
| 		padding: 0 $interiorMargin; | ||||
| 	} | ||||
|     .close { | ||||
| 		//@include test(red, 0.7); | ||||
| 		cursor: pointer; | ||||
|         font-size: 7px; | ||||
| 		width: 8px; | ||||
| @@ -236,132 +239,147 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| @mixin messageBlock($iconW: 32px) { | ||||
|     .type-icon.message-type { | ||||
| /******************************************************************* MESSAGES */ | ||||
|  | ||||
| /* Contexts: | ||||
|     In .t-message-list | ||||
|     In .overlay as a singleton | ||||
|     Inline in the view area | ||||
|  */ | ||||
|  | ||||
| // Archetypal message | ||||
| .l-message { | ||||
|     $iconW: 32px; | ||||
|     @include display(flex); | ||||
|     @include flex-direction(row); | ||||
|     @include align-items(stretch); | ||||
|     padding: $interiorMarginLg; | ||||
|  | ||||
|     &:before { | ||||
|         // Icon | ||||
|         @include flex(0 1 auto); | ||||
|         @include txtShdw($shdwStatusIc); | ||||
|         @extend .icon-bell; | ||||
|         color: $colorStatusDefault; | ||||
|         font-size: $iconW; | ||||
|         padding: 1px; | ||||
|         width: $iconW + 2; | ||||
|         margin-right: $interiorMarginLg; | ||||
|     } | ||||
|  | ||||
|     .message-severity-info .type-icon.message-type { | ||||
|     &.message-severity-info:before { | ||||
|         @extend .icon-info; | ||||
|         color: $colorInfo; | ||||
|     } | ||||
|     .message-severity-alert .type-icon.message-type { | ||||
|         @extend .icon-bell; | ||||
|  | ||||
|     &.message-severity-alert:before { | ||||
|         color: $colorWarningLo; | ||||
|     } | ||||
|     .message-severity-error .type-icon.message-type { | ||||
|  | ||||
|     &.message-severity-error:before { | ||||
|         @extend .icon-alert-rect; | ||||
|         color: $colorWarningHi; | ||||
|     } | ||||
| } | ||||
| /* Paths: | ||||
|     t-dialog | t-dialog-sm > t-message-single | t-message-list > overlay > holder > contents > l-message > | ||||
|         message-type > (icon) | ||||
|         message-contents > | ||||
|         top-bar > | ||||
|             title | ||||
|             hint | ||||
|         editor > | ||||
|             (if displaying list of messages) | ||||
|             ul > li > l-message > | ||||
|                 ... same as above | ||||
|         bottom-bar | ||||
| */ | ||||
|  | ||||
| .l-message { | ||||
|  | ||||
| .w-message-contents { | ||||
|     @include flex(1 1 auto); | ||||
|     @include display(flex); | ||||
|     @include flex-direction(row); | ||||
|     @include align-items(stretch); | ||||
|     .type-icon.message-type { | ||||
|         @include flex(0 1 auto); | ||||
|         position: relative; | ||||
|     } | ||||
|     .message-contents { | ||||
|         @include flex(1 1 auto); | ||||
|         margin-left: $overlayMargin; | ||||
|         position: relative; | ||||
|     @include flex-direction(column); | ||||
|  | ||||
|         .top-bar, | ||||
|     > div, | ||||
|     > span { | ||||
|         //@include test(red); | ||||
|         margin-bottom: $interiorMargin; | ||||
|     } | ||||
|  | ||||
|     .message-body { | ||||
|         @include flex(1 1 100%); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Singleton in an overlay dialog | ||||
| .t-message-single .l-message, | ||||
| .t-message-single.l-message { | ||||
|         $iconW: 80px; | ||||
|         @include absPosDefault(); | ||||
|         padding: 0; | ||||
|         &:before { | ||||
|             font-size: $iconW; | ||||
|             width: $iconW + 2; | ||||
|         } | ||||
|         .title { | ||||
|             font-size: 1.2em; | ||||
|         } | ||||
| } | ||||
|  | ||||
| // Singleton inline in a view | ||||
| .t-message-inline .l-message, | ||||
| .t-message-inline.l-message { | ||||
|     border-radius: $controlCr; | ||||
|     &.message-severity-info { background-color: rgba($colorInfo, 0.3); } | ||||
|     &.message-severity-alert { background-color: rgba($colorWarningLo, 0.3); } | ||||
|     &.message-severity-error { background-color: rgba($colorWarningHi, 0.3); } | ||||
|  | ||||
|     .w-message-contents.l-message-body-only { | ||||
|         .message-body { | ||||
|             margin-bottom: $interiorMarginLg * 2; | ||||
|             margin-top: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // In a list | ||||
| .t-message-list { | ||||
|     @include absPosDefault(); | ||||
|     @include display(flex); | ||||
|     @include flex-direction(column); | ||||
|  | ||||
| // Message as singleton | ||||
| .t-message-single { | ||||
|     @include messageBlock(80px); | ||||
| } | ||||
|  | ||||
| body.desktop .t-message-single { | ||||
|     .l-message, | ||||
|     .bottom-bar { | ||||
|         @include absPosDefault(); | ||||
|     > div, | ||||
|     > span { | ||||
|         margin-bottom: $interiorMargin; | ||||
|     } | ||||
|  | ||||
|     .bottom-bar { | ||||
|         top: auto; | ||||
|         height: $ovrFooterH; | ||||
|     .w-messages { | ||||
|         @include flex(1 1 100%); | ||||
|         overflow-y: auto; | ||||
|         padding-right: $interiorMargin; | ||||
|     } | ||||
|     // Each message | ||||
|     .l-message { | ||||
|         border-radius: $controlCr; | ||||
|         background: rgba($colorOvrFg, 0.1); | ||||
|         margin-bottom: $interiorMargin; | ||||
|         .hint, | ||||
|         .bottom-bar { | ||||
|             text-align: left; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @include phonePortrait { | ||||
|     .t-message-single { | ||||
|         .l-message { | ||||
|             @include flex-direction(column); | ||||
|             .message-contents { margin-left: 0; } | ||||
|         } | ||||
|         .type-icon.message-type { | ||||
|     .t-message-single .l-message, | ||||
|     .t-message-single.l-message { | ||||
|         @include flex-direction(column); | ||||
|         &:before { | ||||
|             margin-right: 0; | ||||
|             margin-bottom: $interiorMarginLg; | ||||
|             width: 100%; | ||||
|             text-align: center; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .bottom-bar { | ||||
|             text-align: center !important; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Messages in list | ||||
| .t-message-list { | ||||
|     @include messageBlock(32px); | ||||
|  | ||||
|     .message-contents { | ||||
|         .l-message { | ||||
|             border-radius: $controlCr; | ||||
|             background: rgba($colorOvrFg, 0.1); | ||||
|             margin-bottom: $interiorMargin; | ||||
|             padding: $interiorMarginLg; | ||||
|  | ||||
|             .message-contents, | ||||
|             .bottom-bar { | ||||
|                 position: relative; | ||||
|             } | ||||
|  | ||||
|             .message-contents { | ||||
|                 font-size: 0.9em; | ||||
|                 margin-left: $interiorMarginLg; | ||||
|                 .message-action { color: pushBack($colorOvrFg, 20%); } | ||||
|                 .bottom-bar { text-align: left; } | ||||
|             } | ||||
|  | ||||
|             .top-bar, | ||||
|             .message-body { | ||||
|                 margin-bottom: $interiorMarginLg; | ||||
|             text-align: center; | ||||
|             .s-button { | ||||
|                 display: block; | ||||
|                 width: 100%; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| body.desktop .t-message-list { | ||||
|     .message-contents .l-message { margin-right: $interiorMarginLg; } | ||||
|     .w-message-contents { padding-right: $interiorMargin; } | ||||
| } | ||||
|  | ||||
| // Alert elements in views | ||||
|   | ||||
| @@ -19,11 +19,10 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| .l-color-palette { | ||||
| .l-palette { | ||||
| 	$d: 16px; | ||||
| 	$colorsPerRow: 10; | ||||
| 	$m: 1; | ||||
| 	$colorSelectedColor: #fff; | ||||
| 
 | ||||
| 	box-sizing: border-box; | ||||
| 	padding: $interiorMargin !important; | ||||
| @@ -33,46 +32,41 @@ | ||||
| 		line-height: $d; | ||||
| 		width: ($d * $colorsPerRow) + ($m * $colorsPerRow); | ||||
| 
 | ||||
|         &.l-option-row { | ||||
|             margin-bottom: $interiorMargin; | ||||
|             .s-palette-item { | ||||
|                 border-color: $colorPaletteFg; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 		.l-palette-item { | ||||
| 			box-sizing: border-box; | ||||
| 			@include txtShdwSubtle(0.8); | ||||
| 			@include trans-prop-nice-fade(0.25s); | ||||
| 			border: 1px solid transparent; | ||||
| 			color: $colorSelectedColor; | ||||
| 			display: block; | ||||
| 			float: left; | ||||
| 			height: $d; width: $d; | ||||
| 			line-height: $d * 0.9; | ||||
| 			margin: 0 ($m * 1px) ($m * 1px) 0; | ||||
|             position: relative; | ||||
| 			text-align: center; | ||||
|             &:before { | ||||
|                 // Check mark for selected items | ||||
|                 font-size: 0.8em; | ||||
|             } | ||||
| 		} | ||||
| 
 | ||||
| 		.s-palette-item { | ||||
|             border: 1px solid transparent; | ||||
|             color: $colorPaletteFg; | ||||
|             text-shadow: $shdwPaletteFg; | ||||
|             @include trans-prop-nice-fade(0.25s); | ||||
| 			&:hover { | ||||
| 				@include trans-prop-nice-fade(0); | ||||
| 				border-color: $colorSelectedColor !important; | ||||
| 				border-color: $colorPaletteSelected !important; | ||||
| 			} | ||||
|             &.selected { | ||||
|                 border-color: $colorPaletteSelected; | ||||
|                 box-shadow: $shdwPaletteSelected; //Needed to see selection rect on light colored swatches | ||||
|             } | ||||
| 		} | ||||
| 
 | ||||
| 		.l-palette-item-label { | ||||
| 			margin-left: $interiorMargin; | ||||
| 		} | ||||
| 
 | ||||
| 		&.l-option-row { | ||||
| 			margin-bottom: $interiorMargin; | ||||
| 			.s-palette-item { | ||||
| 				border-color: $colorBodyFg; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| } | ||||
| @@ -20,7 +20,19 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| .section-header { | ||||
|     border-radius: $basicCr; | ||||
|     background: $colorFormSectionHeader; | ||||
|     color: lighten($colorBodyFg, 20%); | ||||
|     font-size: inherit; | ||||
|     margin: $interiorMargin 0; | ||||
|     padding: $formTBPad $formLRPad; | ||||
|     text-transform: uppercase; | ||||
|     .view-control { | ||||
|         display: inline-block; | ||||
|         margin-right: $interiorMargin; | ||||
|         width: 1em; | ||||
|         height: 1em; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .form { | ||||
| @@ -41,15 +53,6 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .section-header { | ||||
|         border-radius: $basicCr; | ||||
|         background: $colorFormSectionHeader; | ||||
|         $c: lighten($colorBodyFg, 20%); | ||||
|         color: $c; | ||||
|         font-size: 0.8em; | ||||
|         padding: $formTBPad $formLRPad; | ||||
|     } | ||||
|  | ||||
| 	.form-row { | ||||
| 		$m: $interiorMargin; | ||||
| 		box-sizing: border-box; | ||||
| @@ -57,9 +60,6 @@ | ||||
|         margin-bottom: $interiorMarginLg * 2; | ||||
| 		padding: $formTBPad 0; | ||||
| 		position: relative; | ||||
|         //&ng-form { | ||||
|         //    display: block; | ||||
|         //} | ||||
|  | ||||
| 		&.first { | ||||
| 			border-top: none; | ||||
| @@ -171,3 +171,106 @@ | ||||
| 		padding: $interiorMargin; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /**************************************************************************** COMPACT FORM */ | ||||
| // ul > li > label, control | ||||
| // Make a new UL for each form section | ||||
| // Allow control-first, controls-below | ||||
| // TO-DO: migrate work in branch ch-plot-styling that users .inspector-config to use classes below instead | ||||
|  | ||||
| .l-compact-form .tree ul li, | ||||
| .l-compact-form ul li { | ||||
|     padding: 2px 0; | ||||
| } | ||||
|  | ||||
|  | ||||
| .l-compact-form { | ||||
|     $labelW: 40%; | ||||
|     $minW: $labelW; | ||||
|     ul { | ||||
|         margin-bottom: $interiorMarginLg; | ||||
|         li { | ||||
|             @include display(flex); | ||||
|             @include flex-wrap(wrap); | ||||
|             @include align-items(center); | ||||
|             label, | ||||
|             .control { | ||||
|                 @include display(flex); | ||||
|             } | ||||
|             label { | ||||
|                 line-height: inherit; | ||||
|                 width: $labelW; | ||||
|             } | ||||
|             .controls { | ||||
|                 @include flex-grow(1); | ||||
|                 margin-left: $interiorMargin; | ||||
|                 input[type="text"], | ||||
|                 input[type="search"], | ||||
|                 input[type="number"], | ||||
|                 .select { | ||||
|                     height: $btnStdH; | ||||
|                     line-height: $btnStdH; | ||||
|                     vertical-align: middle; | ||||
|                 } | ||||
|  | ||||
|                 .e-control { | ||||
|                     // Individual form controls | ||||
|                     &:not(:first-child) { | ||||
|                         margin-left: $interiorMarginSm; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             &.connects-to-previous { | ||||
|                 padding-top: 0; | ||||
|             } | ||||
|  | ||||
|             &.section-header { | ||||
|                 margin-top: $interiorMarginLg; | ||||
|                 border-top: 1px solid $colorFormLines; | ||||
|             } | ||||
|  | ||||
|             &.controls-first { | ||||
|                 .control { | ||||
|                     @include flex-grow(0); | ||||
|                     margin-right: $interiorMargin; | ||||
|                     min-width: 0; | ||||
|                     order: 1; | ||||
|                     width: auto; | ||||
|                 } | ||||
|                 label { | ||||
|                     @include flex-grow(1); | ||||
|                     order: 2; | ||||
|                     width: auto; | ||||
|                 } | ||||
|             } | ||||
|             &.controls-under { | ||||
|                 display: block; | ||||
|                 .control, label { | ||||
|                     display: block; | ||||
|                     width: auto; | ||||
|                 } | ||||
|  | ||||
|                 ul li { | ||||
|                     border-top: none !important; | ||||
|                     padding: 0; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .form-error { | ||||
|         // Block element that visually flags an error and contains a message | ||||
|         background-color: $colorFormFieldErrorBg; | ||||
|         color: $colorFormFieldErrorFg; | ||||
|         border-radius: $basicCr; | ||||
|         display: block; | ||||
|         padding: 1px 6px; | ||||
|         &:before { | ||||
|             content: $glyph-icon-alert-triangle; | ||||
|             display: inline; | ||||
|             font-family: symbolsfont; | ||||
|             margin-right: $interiorMarginSm; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -79,6 +79,7 @@ | ||||
|  | ||||
|     // Dialog boxes, size constrained and centered in desktop/tablet | ||||
|     &.l-dialog { | ||||
|         font-size: 0.8rem; | ||||
|         .s-button { | ||||
|             &:not(.major) { | ||||
|                 @include btnSubtle($bg: $colorOvrBtnBg, $bgHov: pullForward($colorOvrBtnBg, 10%), $fg: $colorOvrBtnFg, $fgHov: $colorOvrBtnFg, $ic: $colorOvrBtnFg, $icHov: $colorOvrBtnFg); | ||||
| @@ -125,9 +126,9 @@ | ||||
|             @include containerSubtle($colorOvrBg, $colorOvrFg); | ||||
|         } | ||||
|  | ||||
|         .title { | ||||
|         .dialog-title { | ||||
|             @include ellipsize(); | ||||
|             font-size: 1.2em; | ||||
|             font-size: 1.5em; | ||||
|             line-height: 120%; | ||||
|             margin-bottom: $interiorMargin; | ||||
|         } | ||||
|   | ||||
| @@ -52,21 +52,13 @@ ul.tree { | ||||
|  | ||||
|     .view-control { | ||||
|         color: $colorItemTreeVC; | ||||
|         font-size: 0.75em; | ||||
|         margin-right: $interiorMargin; | ||||
|         height: 100%; | ||||
|         line-height: inherit; | ||||
|         width: $treeVCW; | ||||
|         &:before { display: none; } | ||||
|         &.has-children { | ||||
|             &:before { | ||||
|                 position: absolute; | ||||
|                 @include trans-prop-nice(transform, 100ms); | ||||
|                 content: "\e904"; | ||||
|                 @include transform-origin(center); | ||||
|             } | ||||
|             &.expanded:before { | ||||
|                 @include transform(rotate(90deg)); | ||||
|             } | ||||
|             &:before { display: block; } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,10 @@ | ||||
|  | ||||
|     &.t-object-type-timer, | ||||
|     &.t-object-type-clock, | ||||
|     &.t-object-type-hyperlink { | ||||
|     &.t-object-type-hyperlink, | ||||
|     &.t-object-type-summary-widget, | ||||
|     &.no-frame .t-object-type-fixed-display, | ||||
|     &.no-frame .t-object-type-layout { | ||||
|         // Hide the right side buttons for objects where they don't make sense | ||||
|         // Note that this will hide the view Switcher button if applied | ||||
|         // to an object that has it. | ||||
| @@ -103,7 +106,7 @@ | ||||
|         } | ||||
|         &.t-frame-outer > .t-rep-frame { | ||||
|             &.contents { | ||||
|                 $m: 2px; | ||||
|                 $m: 0px; | ||||
|                 top: $m; | ||||
|                 right: $m; | ||||
|                 bottom: $m; | ||||
| @@ -125,14 +128,21 @@ | ||||
|         pointer-events: none !important; | ||||
|     } | ||||
|  | ||||
|         /********************************************************** OBJECT TYPES */ | ||||
|     .t-object-type-hyperlink { | ||||
|     /********************************************************** OBJECT TYPES */ | ||||
|     .t-object-type-hyperlink, | ||||
|     .t-object-type-summary-widget { | ||||
|         .object-holder { | ||||
|             overflow: hidden; | ||||
|         } | ||||
|         .w-summary-widget, | ||||
|         .l-summary-widget, | ||||
|         .l-hyperlink.s-button { | ||||
|             // When a hyperlink is a button in a frame, make it expand to fill out to the object-holder | ||||
|             // Some object types expand to the full size of the object-holder. | ||||
|             @extend .abs; | ||||
|         } | ||||
|  | ||||
|         .l-summary-widget, | ||||
|         .l-hyperlink.s-button { | ||||
|             .label { | ||||
|                 @include ellipsize(); | ||||
|                 @include transform(translateY(-50%)); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| .s-hover-border { | ||||
|     border: 1px dotted transparent; | ||||
|     border: none; | ||||
| } | ||||
|  | ||||
| .s-status-editing { | ||||
|   | ||||
| @@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg; | ||||
| $colorCalCellSelectedFg: $colorItemTreeSelectedFg; | ||||
| $colorCalCellInMonthBg: pushBack($colorMenuBg, 5%); | ||||
|  | ||||
| // Palettes | ||||
| $colorPaletteFg: pullForward($colorMenuBg, 30%); | ||||
| $colorPaletteSelected: #fff; | ||||
| $shdwPaletteFg: black 0 0 2px; | ||||
| $shdwPaletteSelected: inset 0 0 0 1px #000; | ||||
|  | ||||
| // About Screen | ||||
| $colorAboutLink: #84b3ff; | ||||
|  | ||||
|   | ||||
| @@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg; | ||||
| $colorCalCellSelectedFg: $colorItemTreeSelectedFg; | ||||
| $colorCalCellInMonthBg: pullForward($colorMenuBg, 5%); | ||||
|  | ||||
| // Palettes | ||||
| $colorPaletteFg: pullForward($colorMenuBg, 30%); | ||||
| $colorPaletteSelected: #333; | ||||
| $shdwPaletteFg: none; | ||||
| $shdwPaletteSelected: inset 0 0 0 1px #fff; | ||||
|  | ||||
| // About Screen | ||||
| $colorAboutLink: #84b3ff; | ||||
|  | ||||
|   | ||||
| @@ -23,10 +23,13 @@ | ||||
| define([ | ||||
|     "moment-timezone", | ||||
|     "./src/indicators/ClockIndicator", | ||||
|     "./src/indicators/FollowIndicator", | ||||
|     "./src/services/TickerService", | ||||
|     "./src/services/TimerService", | ||||
|     "./src/controllers/ClockController", | ||||
|     "./src/controllers/TimerController", | ||||
|     "./src/controllers/RefreshingController", | ||||
|     "./src/actions/FollowTimerAction", | ||||
|     "./src/actions/StartTimerAction", | ||||
|     "./src/actions/RestartTimerAction", | ||||
|     "./src/actions/StopTimerAction", | ||||
| @@ -37,10 +40,13 @@ define([ | ||||
| ], function ( | ||||
|     MomentTimezone, | ||||
|     ClockIndicator, | ||||
|     FollowIndicator, | ||||
|     TickerService, | ||||
|     TimerService, | ||||
|     ClockController, | ||||
|     TimerController, | ||||
|     RefreshingController, | ||||
|     FollowTimerAction, | ||||
|     StartTimerAction, | ||||
|     RestartTimerAction, | ||||
|     StopTimerAction, | ||||
| @@ -80,6 +86,11 @@ define([ | ||||
|                         "CLOCK_INDICATOR_FORMAT" | ||||
|                     ], | ||||
|                     "priority": "preferred" | ||||
|                 }, | ||||
|                 { | ||||
|                     "implementation": FollowIndicator, | ||||
|                     "depends": ["timerService"], | ||||
|                     "priority": "fallback" | ||||
|                 } | ||||
|             ], | ||||
|             "services": [ | ||||
| @@ -90,6 +101,11 @@ define([ | ||||
|                         "$timeout", | ||||
|                         "now" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "timerService", | ||||
|                     "implementation": TimerService, | ||||
|                     "depends": ["openmct"] | ||||
|                 } | ||||
|             ], | ||||
|             "controllers": [ | ||||
| @@ -134,6 +150,15 @@ define([ | ||||
|                 } | ||||
|             ], | ||||
|             "actions": [ | ||||
|                 { | ||||
|                     "key": "timer.follow", | ||||
|                     "implementation": FollowTimerAction, | ||||
|                     "depends": ["timerService"], | ||||
|                     "category": "contextual", | ||||
|                     "name": "Follow Timer", | ||||
|                     "cssClass": "icon-clock", | ||||
|                     "priority": "optional" | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "timer.start", | ||||
|                     "implementation": StartTimerAction, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
| @@ -20,26 +20,35 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| define([], function () { | ||||
|     function Region(element) { | ||||
|         this.activeView = undefined; | ||||
|         this.element = element; | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
| 
 | ||||
|         /** | ||||
|          * Designates a specific timer for following. Timelines, for example, | ||||
|          * use the actively followed timer to display a time-of-interest line | ||||
|          * and interpret time conductor bounds in the Timeline's relative | ||||
|          * time frame. | ||||
|          * | ||||
|          * @implements {Action} | ||||
|          * @memberof platform/features/clock | ||||
|          * @constructor | ||||
|          * @param {ActionContext} context the context for this action | ||||
|          */ | ||||
|         function FollowTimerAction(timerService, context) { | ||||
|             var domainObject = context.domainObject; | ||||
|             this.perform = | ||||
|                 timerService.setTimer.bind(timerService, domainObject); | ||||
|         } | ||||
| 
 | ||||
|         FollowTimerAction.appliesTo = function (context) { | ||||
|             var model = | ||||
|                 (context.domainObject && context.domainObject.getModel()) || | ||||
|                 {}; | ||||
| 
 | ||||
|             return model.type === 'timer'; | ||||
|         }; | ||||
| 
 | ||||
|         return FollowTimerAction; | ||||
|     } | ||||
| 
 | ||||
|     Region.prototype.clear = function () { | ||||
|         if (this.activeView) { | ||||
|             this.activeView.destroy(); | ||||
|             this.activeView = undefined; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     Region.prototype.show = function (view) { | ||||
|         this.clear(); | ||||
|         this.activeView = view; | ||||
|         if (this.activeView) { | ||||
|             this.activeView.show(this.element); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return Region; | ||||
| }); | ||||
| ); | ||||
							
								
								
									
										57
									
								
								platform/features/clock/src/indicators/FollowIndicator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								platform/features/clock/src/indicators/FollowIndicator.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['moment'], | ||||
|     function (moment) { | ||||
|         var NO_TIMER = "No timer being followed"; | ||||
|  | ||||
|         /** | ||||
|          * Indicator that displays the active timer, as well as its | ||||
|          * current state. | ||||
|          * @implements {Indicator} | ||||
|          * @memberof platform/features/clock | ||||
|          */ | ||||
|         function FollowIndicator(timerService) { | ||||
|             this.timerService = timerService; | ||||
|         } | ||||
|  | ||||
|         FollowIndicator.prototype.getGlyphClass = function () { | ||||
|             return ""; | ||||
|         }; | ||||
|  | ||||
|         FollowIndicator.prototype.getCssClass = function () { | ||||
|             return (this.timerService.getTimer()) ? "icon-timer s-status-ok" : "icon-timer"; | ||||
|         }; | ||||
|  | ||||
|         FollowIndicator.prototype.getText = function () { | ||||
|             var timer = this.timerService.getTimer(); | ||||
|             return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER; | ||||
|         }; | ||||
|  | ||||
|         FollowIndicator.prototype.getDescription = function () { | ||||
|             return ""; | ||||
|         }; | ||||
|  | ||||
|         return FollowIndicator; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										102
									
								
								platform/features/clock/src/services/TimerService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								platform/features/clock/src/services/TimerService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['EventEmitter'], function (EventEmitter) { | ||||
|  | ||||
|     /** | ||||
|      * Tracks the currently-followed Timer object. Used by | ||||
|      * timelines et al to synchronize to a particular timer. | ||||
|      * | ||||
|      * The TimerService emits `change` events when the active timer | ||||
|      * is changed. | ||||
|      */ | ||||
|     function TimerService(openmct) { | ||||
|         EventEmitter.apply(this); | ||||
|         this.time = openmct.time; | ||||
|     } | ||||
|  | ||||
|     TimerService.prototype = Object.create(EventEmitter.prototype); | ||||
|  | ||||
|     /** | ||||
|      * Set (or clear, if `timer` is undefined) the currently active timer. | ||||
|      * @param {DomainObject} timer the new active timer | ||||
|      * @emits change | ||||
|      */ | ||||
|     TimerService.prototype.setTimer = function (timer) { | ||||
|         this.timer = timer; | ||||
|         this.emit('change'); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the currently active timer. | ||||
|      * @return {DomainObject} the active timer | ||||
|      * @emits change | ||||
|      */ | ||||
|     TimerService.prototype.getTimer = function () { | ||||
|         return this.timer; | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Check if there is a currently active timer. | ||||
|      * @return {boolean} true if there is a timer | ||||
|      */ | ||||
|     TimerService.prototype.hasTimer = function () { | ||||
|         return !!this.timer; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Convert the provided timestamp to milliseconds relative to | ||||
|      * the active timer. | ||||
|      * @return {number} milliseconds since timer start | ||||
|      */ | ||||
|     TimerService.prototype.convert = function (timestamp) { | ||||
|         var clock = this.time.clock(); | ||||
|         var canConvert = this.hasTimer() && | ||||
|             !!clock && | ||||
|             this.timer.getModel().timerState !== 'stopped'; | ||||
|  | ||||
|         if (!canConvert) { | ||||
|             return undefined; | ||||
|         } | ||||
|  | ||||
|         var now = clock.currentValue(); | ||||
|         var model = this.timer.getModel(); | ||||
|         var delta = model.timerState === 'paused' ? now - model.pausedTime : 0; | ||||
|         var epoch = model.timestamp; | ||||
|  | ||||
|         return timestamp - epoch - delta; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the value of the active clock, adjusted to be relative to the active | ||||
|      * timer. If there is no clock or no active timer, this will return | ||||
|      * `undefined`. | ||||
|      * @return {number} milliseconds since the start of the active timer | ||||
|      */ | ||||
|     TimerService.prototype.now = function () { | ||||
|         var clock = this.time.clock(); | ||||
|         return clock && this.convert(clock.currentValue()); | ||||
|     }; | ||||
|  | ||||
|     return TimerService; | ||||
| }); | ||||
| @@ -0,0 +1,80 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "../../src/actions/FollowTimerAction" | ||||
| ], function (FollowTimerAction) { | ||||
|     var TIMER_SERVICE_METHODS = | ||||
|         ['setTimer', 'getTimer', 'clearTimer', 'on', 'off']; | ||||
|  | ||||
|     describe("The Follow Timer action", function () { | ||||
|         var testContext; | ||||
|         var testModel; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testModel = {}; | ||||
|             testContext = { domainObject: { getModel: function () { | ||||
|                 return testModel; | ||||
|             } } }; | ||||
|         }); | ||||
|  | ||||
|         it("is applicable to timers", function () { | ||||
|             testModel.type = "timer"; | ||||
|             expect(FollowTimerAction.appliesTo(testContext)).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("is inapplicable to non-timers", function () { | ||||
|             testModel.type = "folder"; | ||||
|             expect(FollowTimerAction.appliesTo(testContext)).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         describe("when instantiated", function () { | ||||
|             var mockTimerService; | ||||
|             var action; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTimerService = jasmine.createSpyObj( | ||||
|                     'timerService', | ||||
|                     TIMER_SERVICE_METHODS | ||||
|                 ); | ||||
|                 action = new FollowTimerAction(mockTimerService, testContext); | ||||
|             }); | ||||
|  | ||||
|             it("does not interact with the timer service", function () { | ||||
|                 TIMER_SERVICE_METHODS.forEach(function (method) { | ||||
|                     expect(mockTimerService[method]).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("and performed", function () { | ||||
|                 beforeEach(function () { | ||||
|                     action.perform(); | ||||
|                 }); | ||||
|  | ||||
|                 it("sets the active timer", function () { | ||||
|                     expect(mockTimerService.setTimer) | ||||
|                         .toHaveBeenCalledWith(testContext.domainObject); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,61 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) { | ||||
|     var TIMER_SERVICE_METHODS = | ||||
|         ['setTimer', 'getTimer', 'clearTimer', 'on', 'off']; | ||||
|  | ||||
|     describe("The timer-following indicator", function () { | ||||
|         var mockTimerService; | ||||
|         var indicator; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockTimerService = | ||||
|                 jasmine.createSpyObj('timerService', TIMER_SERVICE_METHODS); | ||||
|             indicator = new FollowIndicator(mockTimerService); | ||||
|         }); | ||||
|  | ||||
|         it("implements the Indicator interface", function () { | ||||
|             expect(indicator.getGlyphClass()).toEqual(jasmine.any(String)); | ||||
|             expect(indicator.getCssClass()).toEqual(jasmine.any(String)); | ||||
|             expect(indicator.getText()).toEqual(jasmine.any(String)); | ||||
|             expect(indicator.getDescription()).toEqual(jasmine.any(String)); | ||||
|         }); | ||||
|  | ||||
|         describe("when a timer is set", function () { | ||||
|             var testModel; | ||||
|             var mockDomainObject; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testModel = { name: "some timer!" }; | ||||
|                 mockDomainObject = jasmine.createSpyObj('timer', ['getModel']); | ||||
|                 mockDomainObject.getModel.andReturn(testModel); | ||||
|                 mockTimerService.getTimer.andReturn(mockDomainObject); | ||||
|             }); | ||||
|  | ||||
|             it("displays the timer's name", function () { | ||||
|                 expect(indicator.getText().indexOf(testModel.name)) | ||||
|                     .not.toEqual(-1); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										63
									
								
								platform/features/clock/test/services/TimerServiceSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								platform/features/clock/test/services/TimerServiceSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     '../../src/services/TimerService' | ||||
| ], function (TimerService) { | ||||
|     describe("TimerService", function () { | ||||
|         var callback; | ||||
|         var mockmct; | ||||
|         var timerService; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             callback = jasmine.createSpy('callback'); | ||||
|             mockmct = { time: { clock: jasmine.createSpy('clock') } }; | ||||
|             timerService = new TimerService(mockmct); | ||||
|             timerService.on('change', callback); | ||||
|         }); | ||||
|  | ||||
|         it("initially emits no change events", function () { | ||||
|             expect(callback).not.toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it("reports no current timer", function () { | ||||
|             expect(timerService.getTimer()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         describe("setTimer", function () { | ||||
|             var testTimer; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testTimer = { name: "I am some timer; you are nobody." }; | ||||
|                 timerService.setTimer(testTimer); | ||||
|             }); | ||||
|  | ||||
|             it("emits a change event", function () { | ||||
|                 expect(callback).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("reports the current timer", function () { | ||||
|                 expect(timerService.getTimer()).toBe(testTimer); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -19,7 +19,7 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="frame frame-template t-frame-inner abs t-object-type-{{ representation.selected.key }}"> | ||||
| <div class="frame frame-template t-frame-inner abs t-object-type-{{ domainObject.getModel().type }}"> | ||||
|     <div class="abs object-browse-bar l-flex-row"> | ||||
|         <div class="left flex-elem l-flex-row grows"> | ||||
|             <mct-representation | ||||
|   | ||||
| @@ -24,7 +24,7 @@ define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         var DIGITS = 3; | ||||
|         var DIGITS = 2; | ||||
|  | ||||
|         /** | ||||
|          * Wraps a `TelemetryFormatter` to provide formats for domain and | ||||
|   | ||||
| @@ -29,6 +29,7 @@ define([ | ||||
|     "./src/controllers/TimelineTickController", | ||||
|     "./src/controllers/TimelineTableController", | ||||
|     "./src/controllers/TimelineGanttController", | ||||
|     "./src/controllers/TimelineTOIController", | ||||
|     "./src/controllers/ActivityModeValuesController", | ||||
|     "./src/capabilities/ActivityTimespanCapability", | ||||
|     "./src/capabilities/TimelineTimespanCapability", | ||||
| @@ -59,6 +60,7 @@ define([ | ||||
|     TimelineTickController, | ||||
|     TimelineTableController, | ||||
|     TimelineGanttController, | ||||
|     TimelineTOIController, | ||||
|     ActivityModeValuesController, | ||||
|     ActivityTimespanCapability, | ||||
|     TimelineTimespanCapability, | ||||
| @@ -502,6 +504,15 @@ define([ | ||||
|                         "TIMELINE_MAXIMUM_OFFSCREEN" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "TimelineTOIController", | ||||
|                     "implementation": TimelineTOIController, | ||||
|                     "depends": [ | ||||
|                         "openmct", | ||||
|                         "timerService", | ||||
|                         "$scope" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "ActivityModeValuesController", | ||||
|                     "implementation": ActivityModeValuesController, | ||||
|   | ||||
| @@ -29,6 +29,44 @@ | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Follow Line | ||||
|     .l-follow-line { | ||||
|         // TODO: move before and after into l-timeline-gantt so those only render in that pane | ||||
|         pointer-events: none; | ||||
|         position: absolute; | ||||
|         top: 0; bottom: 0; | ||||
|         width: 1px; | ||||
|         z-index: 9; // Just below .l-hover-btns-holder | ||||
|     } | ||||
| } | ||||
|  | ||||
| .l-timeline-gantt { | ||||
|     .l-follow-line { | ||||
|         $d: 0.8rem; | ||||
|         top: $interiorMargin; | ||||
|         &:before, | ||||
|         &:after { | ||||
|             content: ''; | ||||
|             display: block; | ||||
|             height: $d; | ||||
|             width: $d; | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             @include transform(translateX(-50%)); | ||||
|         } | ||||
|         &:before { | ||||
|             // Icon blocker | ||||
|             width: 2 * $d; | ||||
|         } | ||||
|         &:after { | ||||
|             // Icon | ||||
|             font-size: $d; | ||||
|             line-height: $d; | ||||
|             text-align: center; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| .s-timeline-gantt { | ||||
| @@ -108,10 +146,9 @@ | ||||
|     } | ||||
|     .s-hover-btns-holder { | ||||
|         $bg: $timelineHeaderColorBg; | ||||
|         $bga: 1; | ||||
|         $l: 5%; | ||||
|         @include user-select(none); | ||||
|         @include background-image(linear-gradient(-90deg, rgba($bg, $bga), rgba($bg, $bga) 70%, rgba($bg, 0) 100%)); | ||||
|         @include background-image(linear-gradient(-90deg, rgba($bg, 1), rgba($bg, 1) 70%, rgba($bg, 0) 100%)); | ||||
|         .s-button { | ||||
|             height: 16px; | ||||
|             line-height: 16px; | ||||
| @@ -129,4 +166,27 @@ | ||||
|             color: $timelineResourceGraphFg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .s-follow-line { | ||||
|         background: rgba($timeControllerToiLineColor, 0.5); | ||||
|     } | ||||
|  | ||||
|     .s-timeline-gantt { | ||||
|         .s-follow-line { | ||||
|             &:after { | ||||
|                 // Icon | ||||
|                 color: $timeControllerToiLineColor; | ||||
|                 content: $glyph-icon-timer; | ||||
|                 font-family: symbolsfont; | ||||
|                 text-shadow: $shdwItemText; | ||||
|             } | ||||
|             &:before { | ||||
|                 // Blocker | ||||
|                 $bg: $timelineHeaderColorBg; | ||||
|                 $l: 30%; | ||||
|                 @include background-image(linear-gradient(90deg, rgba($bg, 0), rgba($bg, 1) $l, rgba($bg, 1) 100% - $l, rgba($bg, 0))); | ||||
|  | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -75,6 +75,10 @@ | ||||
|                 } | ||||
|             } | ||||
|             &.l-timeline-gantt { | ||||
|                 .abs.l-timeline-gantt-header-w { | ||||
|                     overflow: hidden; | ||||
|                     height: $timelineTopPaneHeaderH; | ||||
|                 } | ||||
|                 .l-swimlanes-holder { | ||||
|                     @include scrollV(scroll); | ||||
|                     bottom: $scrollbarTrackSize; | ||||
|   | ||||
| @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; | ||||
|  | ||||
| @import "../../../../commonUI/general/res/sass/constants"; | ||||
| @import "../../../../commonUI/general/res/sass/mixins"; | ||||
| @import "../../../../commonUI/general/res/sass/glyphs"; | ||||
| @import "../../../../commonUI/themes/espresso/res/sass/constants"; | ||||
| @import "../../../../commonUI/themes/espresso/res/sass/mixins"; | ||||
| @import "constants"; | ||||
|   | ||||
| @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; | ||||
|  | ||||
| @import "../../../../commonUI/general/res/sass/constants"; | ||||
| @import "../../../../commonUI/general/res/sass/mixins"; | ||||
| @import "../../../../commonUI/general/res/sass/glyphs"; | ||||
| @import "../../../../commonUI/themes/snow/res/sass/constants"; | ||||
| @import "../../../../commonUI/themes/snow/res/sass/mixins"; | ||||
| @import "constants"; | ||||
|   | ||||
| @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; | ||||
|  | ||||
| @import "../../../../commonUI/general/res/sass/constants"; | ||||
| @import "../../../../commonUI/general/res/sass/mixins"; | ||||
| @import "../../../../commonUI/general/res/sass/glyphs"; | ||||
| @import "../../../../commonUI/themes/espresso/res/sass/constants"; | ||||
| @import "../../../../commonUI/themes/espresso/res/sass/mixins"; | ||||
| @import "constants"; | ||||
|   | ||||
| @@ -96,109 +96,124 @@ | ||||
|  | ||||
|     <!-- RIGHT PANE: GANTT AND RESOURCE PLOTS --> | ||||
|     <span ng-controller="TimelineZoomController as zoomController" class="abs"> | ||||
|     <mct-split-pane anchor="bottom" | ||||
|  | ||||
|         <span class="toi-control-holder temp" ng-controller="TimelineTOIController as toiController"> | ||||
|             <mct-split-pane anchor="bottom" | ||||
|                     position="pane.y" | ||||
|                     class="abs split-pane-component l-timeline-pane l-pane-r t-pane-v"> | ||||
|  | ||||
|         <!-- TOP PANE GANTT BARS --> | ||||
|         <div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt"> | ||||
|             <div class="l-hover-btns-holder s-hover-btns-holder"> | ||||
|                 <a class="s-button icon-arrows-out" | ||||
|                    ng-click="zoomController.fit()" | ||||
|                    ng-show="true" | ||||
|                    title="Zoom to fit"> | ||||
|                 </a> | ||||
|                 <!-- TOP PANE GANTT BARS --> | ||||
|                 <div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt"> | ||||
|                     <div class="l-hover-btns-holder s-hover-btns-holder"> | ||||
|                         <a class="s-button icon-timer" | ||||
|                            ng-click="scroll.follow = true" | ||||
|                            ng-show="!toiController.isFollowing() && toiController.isActive()" | ||||
|                            title="Follow time bounds"> | ||||
|                         </a> | ||||
|  | ||||
|                 <a class="s-button icon-magnify-in" | ||||
|                    ng-click="zoomController.zoom(-1)" | ||||
|                    ng-show="true" | ||||
|                    title="Zoom in"> | ||||
|                 </a> | ||||
|                         <a class="s-button icon-arrows-out" | ||||
|                            ng-click="scroll.follow = false; zoomController.fit()" | ||||
|                            ng-show="true" | ||||
|                            title="Zoom to fit"> | ||||
|                         </a> | ||||
|  | ||||
|                 <a class="s-button icon-magnify-out" | ||||
|                    ng-click="zoomController.zoom(1)" | ||||
|                    ng-show="true" | ||||
|                    title="Zoom out"> | ||||
|                 </a> | ||||
|             </div> | ||||
|                         <a class="s-button icon-magnify-in" | ||||
|                            ng-click="scroll.follow = false; zoomController.zoom(-1)" | ||||
|                            ng-show="true" | ||||
|                            title="Zoom in"> | ||||
|                         </a> | ||||
|  | ||||
|             <div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x"> | ||||
|                 <mct-include key="'timeline-ticks'" | ||||
|                              parameters="{ | ||||
|                                          fullWidth: zoomController.width(timelineController.end()), | ||||
|                                          start: scroll.x, | ||||
|                                          width: scroll.width, | ||||
|                                          step: zoomController.toPixels(zoomController.zoom()), | ||||
|                                          toMillis: zoomController.toMillis | ||||
|                                          }"> | ||||
|                 </mct-include> | ||||
|             </div> | ||||
|                         <a class="s-button icon-magnify-out" | ||||
|                            ng-click="scroll.follow = false; zoomController.zoom(1)" | ||||
|                            ng-show="true" | ||||
|                            title="Zoom out"> | ||||
|                         </a> | ||||
|                     </div> | ||||
|  | ||||
|             <div class="t-swimlanes-holder l-swimlanes-holder" | ||||
|                  mct-scroll-x="scroll.x" | ||||
|                  mct-scroll-y="scroll.y"> | ||||
|                 <div class="l-width-control" | ||||
|                      ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                     <div class="t-swimlane s-swimlane l-swimlane" | ||||
|                          ng-repeat="swimlane in timelineController.swimlanes()" | ||||
|                          ng-class="{ | ||||
|                                    exceeded: swimlane.exceeded(), | ||||
|                                    selected: selection.selected(swimlane), | ||||
|                                    'drop-into': swimlane.highlight(), | ||||
|                                    'drop-after': swimlane.highlightBottom() | ||||
|                                    }" | ||||
|                          ng-click="selection.select(swimlane)" | ||||
|                          mct-swimlane-drop="swimlane"> | ||||
|                     <div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x"> | ||||
|                         <mct-include key="'timeline-ticks'" | ||||
|                                      parameters="{ | ||||
|                                                  fullWidth: zoomController.width(timelineController.end()), | ||||
|                                                  start: scroll.x, | ||||
|                                                  width: scroll.width, | ||||
|                                                  step: zoomController.toPixels(zoomController.zoom()), | ||||
|                                                  toMillis: zoomController.toMillis | ||||
|                                                  }"> | ||||
|                         </mct-include> | ||||
|                     </div> | ||||
|                     <div ng-if="toiController.isActive()" class="l-follow-line s-follow-line" | ||||
|                          ng-style="{ left: toiController.x() - scroll.x + 'px' }"></div> | ||||
|  | ||||
|                         <mct-representation key="'gantt'" | ||||
|                                             mct-object="swimlane.domainObject" | ||||
|                                             parameters="{ | ||||
|                                                         scroll: scroll, | ||||
|                                                         toPixels: zoomController.toPixels | ||||
|                                                         }"> | ||||
|                         </mct-representation> | ||||
|                     <div class="t-swimlanes-holder l-swimlanes-holder" | ||||
|                          mct-scroll-x="scroll.x" | ||||
|                          mct-scroll-y="scroll.y"> | ||||
|                         <div class="l-width-control" | ||||
|                              ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                             <div class="t-swimlane s-swimlane l-swimlane" | ||||
|                                  ng-repeat="swimlane in timelineController.swimlanes()" | ||||
|                                  ng-class="{ | ||||
|                                            exceeded: swimlane.exceeded(), | ||||
|                                            selected: selection.selected(swimlane), | ||||
|                                            'drop-into': swimlane.highlight(), | ||||
|                                            'drop-after': swimlane.highlightBottom() | ||||
|                                            }" | ||||
|                                  ng-click="selection.select(swimlane)" | ||||
|                                  mct-swimlane-drop="swimlane"> | ||||
|  | ||||
|                         <span ng-if="selection.selected(swimlane)"> | ||||
|                             <span ng-repeat="handle in timelineController.handles()" | ||||
|                                   ng-style="handle.style(zoomController)" | ||||
|                                   style="position: absolute; top: 0px; bottom: 0px;" | ||||
|                                   class="handle" | ||||
|                                   ng-class="{ start: $index === 0, mid: $index === 1, end: $index > 1 }" | ||||
|                                   mct-drag-down="handle.begin()" | ||||
|                                   mct-drag="handle.drag(delta[0], zoomController); timelineController.refresh()" | ||||
|                                   mct-drag-up="handle.finish()"> | ||||
|                             </span> | ||||
|                         </span> | ||||
|                                 <mct-representation key="'gantt'" | ||||
|                                                     mct-object="swimlane.domainObject" | ||||
|                                                     parameters="{ | ||||
|                                                                 scroll: scroll, | ||||
|                                                                 toPixels: zoomController.toPixels | ||||
|                                                                 }"> | ||||
|                                 </mct-representation> | ||||
|  | ||||
|                                 <span ng-if="selection.selected(swimlane)"> | ||||
|                                     <span ng-repeat="handle in timelineController.handles()" | ||||
|                                           ng-style="handle.style(zoomController)" | ||||
|                                           style="position: absolute; top: 0px; bottom: 0px;" | ||||
|                                           class="handle" | ||||
|                                           ng-class="{ start: $index === 0, mid: $index === 1, end: $index > 1 }" | ||||
|                                           mct-drag-down="handle.begin()" | ||||
|                                           mct-drag="handle.drag(delta[0], zoomController); timelineController.refresh()" | ||||
|                                           mct-drag-up="handle.finish()"> | ||||
|                                     </span> | ||||
|                                 </span> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- HORZ SPLITTER --> | ||||
|         <mct-splitter></mct-splitter> | ||||
|                 <!-- HORZ SPLITTER --> | ||||
|                 <mct-splitter></mct-splitter> | ||||
|  | ||||
|         <!-- BOTTOM PANE RESOURCE GRAPHS AND RIGHT PANE HORIZONTAL SCROLL CONTROL --> | ||||
|         <div class="split-pane-component l-timeline-resource-graph l-timeline-pane t-pane-h l-pane-btm"> | ||||
|             <div class="l-graphs-holder" | ||||
|                  mct-resize="scroll.width = bounds.width"> | ||||
|                 <div class="t-graphs l-graphs"> | ||||
|                     <mct-include key="'timeline-resource-graphs'" | ||||
|                                  parameters="{ | ||||
|                                              origin: zoomController.toMillis(scroll.x), | ||||
|                                              duration: zoomController.toMillis(scroll.width), | ||||
|                                              graphs: timelineController.graphs() | ||||
|                                              }"> | ||||
|                     </mct-include> | ||||
|                 <!-- BOTTOM PANE RESOURCE GRAPHS AND RIGHT PANE HORIZONTAL SCROLL CONTROL --> | ||||
|                 <div class="split-pane-component l-timeline-resource-graph l-timeline-pane t-pane-h l-pane-btm"> | ||||
|                     <div class="l-graphs-holder" | ||||
|                          mct-resize="scroll.width = bounds.width"> | ||||
|                         <div class="t-graphs l-graphs"> | ||||
|                             <mct-include key="'timeline-resource-graphs'" | ||||
|                                          parameters="{ | ||||
|                                                      origin: zoomController.toMillis(scroll.x), | ||||
|                                                      duration: zoomController.toMillis(scroll.width), | ||||
|                                                      graphs: timelineController.graphs() | ||||
|                                                      }"> | ||||
|                             </mct-include> | ||||
|                         </div> | ||||
|                         <div ng-if="toiController.isActive()" class="l-follow-line s-follow-line" | ||||
|                              ng-style="{ left: toiController.x() - scroll.x + 'px' }"></div> | ||||
|                     </div> | ||||
|                     <div mct-scroll-x="scroll.x" | ||||
|                          class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control"> | ||||
|                         <div class="l-width-control" | ||||
|                              ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div mct-scroll-x="scroll.x" | ||||
|                  class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control"> | ||||
|                 <div class="l-width-control" | ||||
|                      ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </mct-split-pane> | ||||
|             </mct-split-pane> | ||||
|         </span> | ||||
|  | ||||
|  | ||||
|     </span> | ||||
| </mct-split-pane> | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,111 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|  | ||||
|     /** | ||||
|      * Tracks time-of-interest in timelines, updating both scroll state | ||||
|      * (when appropriate) and positioning of the displayed line. | ||||
|      */ | ||||
|     function TimelineTOIController(openmct, timerService, $scope) { | ||||
|         this.openmct = openmct; | ||||
|         this.timerService = timerService; | ||||
|         this.$scope = $scope; | ||||
|  | ||||
|         this.change = this.change.bind(this); | ||||
|         this.bounds = this.bounds.bind(this); | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|  | ||||
|         this.timerService.on('change', this.change); | ||||
|         this.openmct.time.on('bounds', this.bounds); | ||||
|  | ||||
|         this.$scope.$on('$destroy', this.destroy); | ||||
|  | ||||
|         this.$scope.scroll.follow = this.timerService.hasTimer(); | ||||
|         if (this.$scope.zoomController) { | ||||
|             this.bounds(this.openmct.time.bounds()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Handle a `change` event from the timer service; track the | ||||
|      * new timer. | ||||
|      */ | ||||
|     TimelineTOIController.prototype.change = function () { | ||||
|         this.$scope.scroll.follow = | ||||
|             this.$scope.scroll.follow || this.timerService.hasTimer(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Handle a `bounds` event from the time API; scroll the timeline | ||||
|      * to match the current bounds, if currently in follow mode. | ||||
|      */ | ||||
|     TimelineTOIController.prototype.bounds = function (bounds) { | ||||
|         if (this.isFollowing()) { | ||||
|             var start = this.timerService.convert(bounds.start); | ||||
|             var end = this.timerService.convert(bounds.end); | ||||
|             this.duration = bounds.end - bounds.start; | ||||
|             this.$scope.zoomController.bounds(start, end); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Handle a `$destroy` event from scope; detach all observers. | ||||
|      */ | ||||
|     TimelineTOIController.prototype.destroy = function () { | ||||
|         this.timerService.off('change', this.change); | ||||
|         this.openmct.time.off('bounds', this.bounds); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the x position of the time-of-interest line, | ||||
|      * in pixels from the left edge of the timeline area. | ||||
|      */ | ||||
|     TimelineTOIController.prototype.x = function () { | ||||
|         var now = this.timerService.now(); | ||||
|  | ||||
|         if (now === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
|  | ||||
|         return this.$scope.zoomController.toPixels(this.timerService.now()); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Check if there is an active time-of-interest to be shown. | ||||
|      * @return {boolean} true when active | ||||
|      */ | ||||
|     TimelineTOIController.prototype.isActive = function () { | ||||
|         return this.x() !== undefined; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Check if the timeline should be following time conductor bounds. | ||||
|      * @return {boolean} true when following | ||||
|      */ | ||||
|     TimelineTOIController.prototype.isFollowing = function () { | ||||
|         return !!this.$scope.scroll.follow && this.timerService.now() !== undefined; | ||||
|     }; | ||||
|  | ||||
|     return TimelineTOIController; | ||||
| }); | ||||
| @@ -32,7 +32,8 @@ define( | ||||
|             // Prefer to start with the middle index | ||||
|             var zoomLevels = ZOOM_CONFIGURATION.levels || [1000], | ||||
|                 zoomIndex = Math.floor(zoomLevels.length / 2), | ||||
|                 tickWidth = ZOOM_CONFIGURATION.width || 200; | ||||
|                 tickWidth = ZOOM_CONFIGURATION.width || 200, | ||||
|                 lastWidth = Number.MAX_VALUE; // Don't constrain prematurely | ||||
|  | ||||
|             function toMillis(pixels) { | ||||
|                 return (pixels / tickWidth) * zoomLevels[zoomIndex]; | ||||
| @@ -55,19 +56,29 @@ define( | ||||
|  | ||||
|             function setScroll(x) { | ||||
|                 $window.requestAnimationFrame(function () { | ||||
|                     $scope.scroll.x = x; | ||||
|                     $scope.scroll.x = Math.min( | ||||
|                         Math.max(x, 0), | ||||
|                         lastWidth - $scope.scroll.width | ||||
|                     ); | ||||
|                     $scope.$apply(); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function initializeZoomFromTimespan(timespan) { | ||||
|                 var timelineDuration = timespan.getDuration(); | ||||
|             function initializeZoomFromStartEnd(start, end) { | ||||
|                 var duration = end - start; | ||||
|                 zoomIndex = 0; | ||||
|                 while (toMillis($scope.scroll.width) < timelineDuration && | ||||
|                 while (toMillis($scope.scroll.width) < duration && | ||||
|                         zoomIndex < zoomLevels.length - 1) { | ||||
|                     zoomIndex += 1; | ||||
|                 } | ||||
|                 setScroll(toPixels(timespan.getStart())); | ||||
|                 setScroll(toPixels(start)); | ||||
|             } | ||||
|  | ||||
|             function initializeZoomFromTimespan(timespan) { | ||||
|                 return initializeZoomFromStartEnd( | ||||
|                     timespan.getStart(), | ||||
|                     timespan.getEnd() | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             function initializeZoom() { | ||||
| @@ -101,6 +112,13 @@ define( | ||||
|                     } | ||||
|                     return zoomLevels[zoomIndex]; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Adjust the current zoom bounds to fit both the | ||||
|                  * start and the end time provided. | ||||
|                  * @param {number} start the starting timestamp | ||||
|                  * @param {number} end the ending timestamp | ||||
|                  */ | ||||
|                 bounds: initializeZoomFromStartEnd, | ||||
|                 /** | ||||
|                  * Set the zoom level to fit the bounds of the timeline | ||||
|                  * being viewed. | ||||
| @@ -119,14 +137,14 @@ define( | ||||
|                  */ | ||||
|                 toMillis: toMillis, | ||||
|                 /** | ||||
|                  * Get the pixel width necessary to fit the specified | ||||
|                  * timestamp, expressed as an offset in milliseconds from | ||||
|                  * the start of the timeline. | ||||
|                  * Set the maximum timestamp value to be displayed, and get | ||||
|                  * the pixel width necessary to display this value. | ||||
|                  * @param {number} timestamp the time to display | ||||
|                  */ | ||||
|                 width: function (timestamp) { | ||||
|                     var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING))); | ||||
|                     return Math.max($scope.scroll.width, pixels); | ||||
|                     lastWidth = Math.max($scope.scroll.width, pixels); | ||||
|                     return lastWidth; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,138 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "../../src/controllers/TimelineTOIController", | ||||
|     "EventEmitter" | ||||
| ], function (TimelineTOIController, EventEmitter) { | ||||
|     describe("The timeline TOI controller", function () { | ||||
|         var mockmct; | ||||
|         var mockTimerService; | ||||
|         var mockScope; | ||||
|         var controller; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockmct = { time: new EventEmitter() }; | ||||
|             mockmct.time.bounds = jasmine.createSpy('bounds'); | ||||
|             mockTimerService = new EventEmitter(); | ||||
|             mockTimerService.getTimer = jasmine.createSpy('getTimer'); | ||||
|             mockTimerService.hasTimer = jasmine.createSpy('hasTimer'); | ||||
|             mockTimerService.now = jasmine.createSpy('now'); | ||||
|             mockTimerService.convert = jasmine.createSpy('convert'); | ||||
|             mockScope = new EventEmitter(); | ||||
|             mockScope.$on = mockScope.on.bind(mockScope); | ||||
|             mockScope.zoomController = jasmine.createSpyObj('zoom', [ | ||||
|                 'bounds', | ||||
|                 'toPixels' | ||||
|             ]); | ||||
|             mockScope.scroll = { x: 10, width: 1000 }; | ||||
|  | ||||
|             spyOn(mockmct.time, "on").andCallThrough(); | ||||
|             spyOn(mockmct.time, "off").andCallThrough(); | ||||
|             spyOn(mockTimerService, "on").andCallThrough(); | ||||
|             spyOn(mockTimerService, "off").andCallThrough(); | ||||
|  | ||||
|             controller = new TimelineTOIController( | ||||
|                 mockmct, | ||||
|                 mockTimerService, | ||||
|                 mockScope | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|         it("reports an undefined x position initially", function () { | ||||
|             expect(controller.x()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         it("listens for bounds changes", function () { | ||||
|             expect(mockmct.time.on) | ||||
|                 .toHaveBeenCalledWith('bounds', controller.bounds); | ||||
|         }); | ||||
|  | ||||
|         it("listens for timer changes", function () { | ||||
|             expect(mockTimerService.on) | ||||
|                 .toHaveBeenCalledWith('change', controller.change); | ||||
|         }); | ||||
|  | ||||
|         it("is not active", function () { | ||||
|             expect(controller.isActive()).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         describe("on $destroy from scope", function () { | ||||
|             beforeEach(function () { | ||||
|                 mockScope.emit("$destroy"); | ||||
|             }); | ||||
|  | ||||
|             it("unregisters listeners", function () { | ||||
|                 expect(mockmct.time.off) | ||||
|                     .toHaveBeenCalledWith('bounds', controller.bounds); | ||||
|                 expect(mockTimerService.off) | ||||
|                     .toHaveBeenCalledWith('change', controller.change); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe("when a timer and timestamp present", function () { | ||||
|             var mockTimer; | ||||
|             var testNow; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testNow = 333221; | ||||
|                 mockScope.zoomController.toPixels | ||||
|                     .andCallFake(function (millis) { | ||||
|                         return millis * 2; | ||||
|                     }); | ||||
|                 mockTimerService.emit('change', mockTimer); | ||||
|                 mockTimerService.now.andReturn(testNow); | ||||
|             }); | ||||
|  | ||||
|             it("reports an x value from the zoomController", function () { | ||||
|                 var now = mockTimerService.now(); | ||||
|                 var expected = mockScope.zoomController.toPixels(now); | ||||
|                 expect(controller.x()).toEqual(expected); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe("when follow mode is disabled", function () { | ||||
|             beforeEach(function () { | ||||
|                 mockScope.scroll.follow = false; | ||||
|             }); | ||||
|  | ||||
|             it("ignores bounds events", function () { | ||||
|                 mockmct.time.emit('bounds', { start: 0, end: 1000 }); | ||||
|                 expect(mockScope.zoomController.bounds) | ||||
|                     .not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe("when follow mode is enabled", function () { | ||||
|             beforeEach(function () { | ||||
|                 mockScope.scroll.follow = true; | ||||
|                 mockTimerService.now.andReturn(500); | ||||
|             }); | ||||
|  | ||||
|             it("zooms on bounds events", function () { | ||||
|                 mockmct.time.emit('bounds', { start: 0, end: 1000 }); | ||||
|                 expect(mockScope.zoomController.bounds) | ||||
|                     .toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -24,21 +24,22 @@ | ||||
|  | ||||
|     <span class="l-click-area" ng-click="toggle.toggle()"></span> | ||||
|     <span class="color-swatch" | ||||
|           ng-class="{'no-selection':ngModel[field] === 'transparent'}" | ||||
|           ng-style="{ | ||||
|              background: ngModel[field] | ||||
|              'background-color': ngModel[field] | ||||
|          }"> | ||||
|     </span> | ||||
|     <span class="title-label" ng-if="structure.text"> | ||||
|         {{structure.text}} | ||||
|     </span> | ||||
|  | ||||
|     <div class="menu l-color-palette" | ||||
|     <div class="menu l-palette l-color-palette" | ||||
|         ng-controller="ColorController as colors" | ||||
|         ng-show="toggle.isActive()"> | ||||
|         <div | ||||
|             class="l-palette-row l-option-row" | ||||
|             ng-if="!structure.mandatory"> | ||||
|             <div class="l-palette-item s-palette-item {{ngModel[field] === 'transparent' ? 'icon-check' : '' }}" | ||||
|             <div class="l-palette-item s-palette-item no-selection {{ngModel[field] === 'transparent' ? 'selected' : '' }}" | ||||
|                 ng-click="ngModel[field] = 'transparent'"> | ||||
|             </div> | ||||
|             <span class="l-palette-item-label">None</span> | ||||
| @@ -46,7 +47,7 @@ | ||||
|         <div | ||||
|             class="l-palette-row" | ||||
|             ng-repeat="group in colors.groups()"> | ||||
|             <div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'icon-check' : '' }}" | ||||
|             <div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'selected' : '' }}" | ||||
|                 ng-repeat="color in group" | ||||
|                 ng-style="{ background: color }" | ||||
|                 ng-click="ngModel[field] = color"> | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -106,9 +106,9 @@ define([ | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name mainViews | ||||
|          * @name objectViews | ||||
|          */ | ||||
|         this.mainViews = new ViewRegistry(); | ||||
|         this.objectViews = new ViewRegistry(); | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the Inspector area. | ||||
| @@ -255,6 +255,19 @@ define([ | ||||
|             this.legacyExtension('types', legacyDefinition); | ||||
|         }.bind(this)); | ||||
|  | ||||
|         this.objectViews.providers.forEach(function (p) { | ||||
|             this.legacyExtension('views', { | ||||
|                 key: 'vpid' + p.vpid, | ||||
|                 vpid: p.vpid, | ||||
|                 provider: p, | ||||
|                 name: p.name, | ||||
|                 cssClass: p.cssClass, | ||||
|                 description: p.description, | ||||
|                 editable: p.editable, | ||||
|                 template: '<mct-view mct-vpid="' + p.vpid + '"/>' | ||||
|             }); | ||||
|         }, this); | ||||
|  | ||||
|         legacyRegistry.register('adapter', this.legacyBundle); | ||||
|         legacyRegistry.enable('adapter'); | ||||
|         /** | ||||
|   | ||||
| @@ -24,7 +24,6 @@ define([ | ||||
|     'legacyRegistry', | ||||
|     './actions/ActionDialogDecorator', | ||||
|     './capabilities/AdapterCapability', | ||||
|     './controllers/AdaptedViewController', | ||||
|     './directives/MCTView', | ||||
|     './services/Instantiate', | ||||
|     './services/MissingModelCompatibilityDecorator', | ||||
| @@ -32,13 +31,11 @@ define([ | ||||
|     './policies/AdapterCompositionPolicy', | ||||
|     './policies/AdaptedViewPolicy', | ||||
|     './runs/AlternateCompositionInitializer', | ||||
|     './runs/TimeSettingsURLHandler', | ||||
|     'text!./templates/adapted-view-template.html' | ||||
|     './runs/TimeSettingsURLHandler' | ||||
| ], function ( | ||||
|     legacyRegistry, | ||||
|     ActionDialogDecorator, | ||||
|     AdapterCapability, | ||||
|     AdaptedViewController, | ||||
|     MCTView, | ||||
|     Instantiate, | ||||
|     MissingModelCompatibilityDecorator, | ||||
| @@ -46,15 +43,15 @@ define([ | ||||
|     AdapterCompositionPolicy, | ||||
|     AdaptedViewPolicy, | ||||
|     AlternateCompositionInitializer, | ||||
|     TimeSettingsURLHandler, | ||||
|     adaptedViewTemplate | ||||
|     TimeSettingsURLHandler | ||||
| ) { | ||||
|     legacyRegistry.register('src/adapter', { | ||||
|         "extensions": { | ||||
|             "directives": [ | ||||
|                 { | ||||
|                     key: "mctView", | ||||
|                     implementation: MCTView | ||||
|                     implementation: MCTView, | ||||
|                     depends: ["openmct"] | ||||
|                 } | ||||
|             ], | ||||
|             capabilities: [ | ||||
| @@ -63,16 +60,6 @@ define([ | ||||
|                     implementation: AdapterCapability | ||||
|                 } | ||||
|             ], | ||||
|             controllers: [ | ||||
|                 { | ||||
|                     key: "AdaptedViewController", | ||||
|                     implementation: AdaptedViewController, | ||||
|                     depends: [ | ||||
|                         '$scope', | ||||
|                         'openmct' | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             services: [ | ||||
|                 { | ||||
|                     key: "instantiate", | ||||
| @@ -135,12 +122,6 @@ define([ | ||||
|                     depends: ["openmct", "$location", "$rootScope"] | ||||
|                 } | ||||
|             ], | ||||
|             views: [ | ||||
|                 { | ||||
|                     key: "adapted-view", | ||||
|                     template: adaptedViewTemplate | ||||
|                 } | ||||
|             ], | ||||
|             licenses: [ | ||||
|                 { | ||||
|                     "name": "almond", | ||||
|   | ||||
| @@ -22,10 +22,12 @@ | ||||
|  | ||||
| define([ | ||||
|     './synchronizeMutationCapability', | ||||
|     './AlternateCompositionCapability' | ||||
|     './AlternateCompositionCapability', | ||||
|     './patchViewCapability' | ||||
| ], function ( | ||||
|     synchronizeMutationCapability, | ||||
|     AlternateCompositionCapability | ||||
|     AlternateCompositionCapability, | ||||
|     patchViewCapability | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -46,6 +48,9 @@ define([ | ||||
|             capabilities.mutation = | ||||
|                 synchronizeMutationCapability(capabilities.mutation); | ||||
|         } | ||||
|         if (capabilities.view) { | ||||
|             capabilities.view = patchViewCapability(capabilities.view); | ||||
|         } | ||||
|         if (AlternateCompositionCapability.appliesTo(model, id)) { | ||||
|             capabilities.composition = function (domainObject) { | ||||
|                 return new AlternateCompositionCapability(this.$injector, domainObject); | ||||
|   | ||||
							
								
								
									
										61
									
								
								src/adapter/capabilities/patchViewCapability.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/adapter/capabilities/patchViewCapability.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     _ | ||||
| ) { | ||||
|  | ||||
|     function patchViewCapability(viewConstructor) { | ||||
|         return function makeCapability(domainObject) { | ||||
|             var capability = viewConstructor(domainObject); | ||||
|             var oldInvoke = capability.invoke.bind(capability); | ||||
|  | ||||
|             capability.invoke = function () { | ||||
|                 var availableViews = oldInvoke(); | ||||
|                 var newDomainObject = capability | ||||
|                     .domainObject | ||||
|                     .useCapability('adapter'); | ||||
|  | ||||
|                 return _(availableViews).map(function (v, i) { | ||||
|                     var vd = { | ||||
|                         view: v, | ||||
|                         priority: i + 100 // arbitrary to allow new views to | ||||
|                         // be defaults by returning priority less than 100. | ||||
|                     }; | ||||
|                     if (v.provider) { | ||||
|                         vd.priority = v.provider.canView(newDomainObject); | ||||
|                     } | ||||
|                     return vd; | ||||
|                 }) | ||||
|                 .sortBy('priority') | ||||
|                 .map('view') | ||||
|                 .value(); | ||||
|             }; | ||||
|             return capability; | ||||
|         }; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     return patchViewCapability; | ||||
| }); | ||||
| @@ -21,18 +21,20 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'angular', | ||||
|     './Region' | ||||
| ], function ( | ||||
|     angular, | ||||
|     Region | ||||
| ) { | ||||
|     function MCTView() { | ||||
|     function MCTView(openmct) { | ||||
|         return { | ||||
|             restrict: 'A', | ||||
|             restrict: 'E', | ||||
|             link: function (scope, element, attrs) { | ||||
|                 var region = new Region(element[0]); | ||||
|                 scope.$watch(attrs.mctView, region.show.bind(region)); | ||||
|                 var provider = openmct.objectViews.getByVPID(Number(attrs.mctVpid)); | ||||
|                 var view = new provider.view(scope.domainObject.useCapability('adapter')); | ||||
|                 view.show(element[0]); | ||||
|                 if (view.destroy) { | ||||
|                     scope.$on('$destroy', function () { | ||||
|                         view.destroy(element[0]); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|   | ||||
| @@ -29,9 +29,9 @@ define([], function () { | ||||
|         view, | ||||
|         legacyObject | ||||
|     ) { | ||||
|         if (view.key === 'adapted-view') { | ||||
|         if (view.hasOwnProperty('vpid')) { | ||||
|             var domainObject = legacyObject.useCapability('adapter'); | ||||
|             return this.openmct.mainViews.get(domainObject).length > 0; | ||||
|             return view.provider.canView(domainObject); | ||||
|         } | ||||
|         return true; | ||||
|     }; | ||||
|   | ||||
| @@ -35,7 +35,7 @@ define([ | ||||
|             name: 'Name' | ||||
|         }); | ||||
|  | ||||
|         metadata.domains.forEach(function (domain, index) { | ||||
|         (metadata.domains || []).forEach(function (domain, index) { | ||||
|             var valueMetadata = _.clone(domain); | ||||
|             valueMetadata.hints = { | ||||
|                 domain: index + 1 | ||||
| @@ -43,11 +43,11 @@ define([ | ||||
|             valueMetadatas.push(valueMetadata); | ||||
|         }); | ||||
|  | ||||
|         metadata.ranges.forEach(function (range, index) { | ||||
|         (metadata.ranges || []).forEach(function (range, index) { | ||||
|             var valueMetadata = _.clone(range); | ||||
|             valueMetadata.hints = { | ||||
|                 range: index, | ||||
|                 priority: index + metadata.domains.length + 1 | ||||
|                 priority: index + (metadata.domains || []).length + 1 | ||||
|             }; | ||||
|  | ||||
|             if (valueMetadata.type === 'enum') { | ||||
|   | ||||
| @@ -25,7 +25,7 @@ define([ | ||||
| ], function ( | ||||
|     _ | ||||
| ) { | ||||
|  | ||||
|      | ||||
|     // TODO: needs reference to formatService; | ||||
|     function TelemetryValueFormatter(valueMetadata, formatService) { | ||||
|         var numberFormatter = { | ||||
| @@ -33,7 +33,12 @@ define([ | ||||
|                 return Number(x); | ||||
|             }, | ||||
|             format: function (x) { | ||||
|                 return x; | ||||
|                 var number = parseFloat(x); | ||||
|                 if (isNaN(number)){ | ||||
|                     return x; | ||||
|                 } else { | ||||
|                     return number.toFixed(2); | ||||
|                 } | ||||
|             }, | ||||
|             validate: function (x) { | ||||
|                 return true; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|         <a class="close icon-x-in-circle"></a> | ||||
|         <div class="abs inner-holder contents"> | ||||
|             <div class="abs top-bar"> | ||||
|                 <div class="title"></div> | ||||
|                 <div class="dialog-title"></div> | ||||
|                 <div class="hint"></div> | ||||
|             </div> | ||||
|             <div class='abs editor'> | ||||
|   | ||||
| @@ -27,7 +27,9 @@ define([ | ||||
|     '../../platform/features/autoflow/plugin', | ||||
|     './timeConductor/plugin', | ||||
|     '../../example/imagery/plugin', | ||||
|     '../../platform/import-export/bundle' | ||||
|     './summaryWidget/plugin', | ||||
|     '../../platform/import-export/bundle', | ||||
|     './telemetryMean/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
|     UTCTimeSystem, | ||||
| @@ -35,7 +37,9 @@ define([ | ||||
|     AutoflowPlugin, | ||||
|     TimeConductorPlugin, | ||||
|     ExampleImagery, | ||||
|     ImportExport | ||||
|     SummaryWidget, | ||||
|     ImportExport, | ||||
|     TelemetryMean | ||||
| ) { | ||||
|     var bundleMap = { | ||||
|         CouchDB: 'platform/persistence/couch', | ||||
| @@ -120,6 +124,8 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     plugins.ExampleImagery = ExampleImagery; | ||||
|     plugins.SummaryWidget = SummaryWidget; | ||||
|     plugins.TelemetryMean = TelemetryMean; | ||||
|  | ||||
|     return plugins; | ||||
| }); | ||||
|   | ||||
| @@ -20,21 +20,33 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| define([], function () { | ||||
|     function AdaptedViewController($scope, openmct) { | ||||
|         function refresh(legacyObject) { | ||||
|             if (!legacyObject) { | ||||
|                 $scope.view = undefined; | ||||
|                 return; | ||||
|             } | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
| 
 | ||||
|             var domainObject = legacyObject.useCapability('adapter'); | ||||
|             var providers = openmct.mainViews.get(domainObject); | ||||
|             $scope.view = providers[0] && providers[0].view(domainObject); | ||||
|         /** | ||||
|          * Defines composition policy for Display Layout objects. | ||||
|          * They cannot contain folders. | ||||
|          * @constructor | ||||
|          * @memberof platform/features/layout | ||||
|          * @implements {Policy.<View, DomainObject>} | ||||
|          */ | ||||
|         function SummaryWidgetsCompositionPolicy(openmct) { | ||||
|             this.openmct = openmct; | ||||
|         } | ||||
| 
 | ||||
|         $scope.$watch('domainObject', refresh); | ||||
|     } | ||||
|         SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { | ||||
| 
 | ||||
|     return AdaptedViewController; | ||||
| }); | ||||
|             var parentType = parent.getCapability('type'); | ||||
|             var newStyleChild = child.useCapability('adapter'); | ||||
| 
 | ||||
|             if (parentType.instanceOf('summary-widget') && !this.openmct.telemetry.canProvideTelemetry(newStyleChild)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         }; | ||||
| 
 | ||||
|         return SummaryWidgetsCompositionPolicy; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										69
									
								
								src/plugins/summaryWidget/plugin.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										69
									
								
								src/plugins/summaryWidget/plugin.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (SummaryWidget, SummaryWidgetsCompositionPolicy) { | ||||
|  | ||||
|     function plugin() { | ||||
|  | ||||
|         var widgetType = { | ||||
|             name: 'Summary Widget', | ||||
|             description: 'A compact status update for collections of telemetry-producing items', | ||||
|             creatable: true, | ||||
|             cssClass: 'icon-summary-widget', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = {}; | ||||
|                 domainObject.openNewTab = 'thisTab'; | ||||
|             }, | ||||
|             form: [ | ||||
|                 { | ||||
|                     "key": "url", | ||||
|                     "name": "URL", | ||||
|                     "control": "textfield", | ||||
|                     "pattern": "^(ftp|https?)\\:\\/\\/", | ||||
|                     "required": false, | ||||
|                     "cssClass": "l-input-lg" | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "openNewTab", | ||||
|                     "name": "Tab to Open Hyperlink", | ||||
|                     "control": "select", | ||||
|                     "options": [ | ||||
|                             { | ||||
|                                 "value": "thisTab", | ||||
|                                 "name": "Open in this tab" | ||||
|                             }, | ||||
|                             { | ||||
|                                 "value": "newTab", | ||||
|                                 "name": "Open in a new tab" | ||||
|                             } | ||||
|                         ], | ||||
|                     "cssClass": "l-inline" | ||||
|                 } | ||||
|             ] | ||||
|         }; | ||||
|  | ||||
|         function initViewProvider(openmct) { | ||||
|             return { | ||||
|                 name: 'Widget View', | ||||
|                 view: function (domainObject) { | ||||
|                     var summaryWidget = new SummaryWidget(domainObject, openmct); | ||||
|                     return { | ||||
|                         show: summaryWidget.show, | ||||
|                         destroy: summaryWidget.destroy | ||||
|                     }; | ||||
|                 }, | ||||
|                 canView: function (domainObject) { | ||||
|                     return (domainObject.type === 'summary-widget'); | ||||
|                 }, | ||||
|                 editable: true | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return function install(openmct) { | ||||
|             openmct.types.addType('summary-widget', widgetType); | ||||
|             openmct.objectViews.addProvider(initViewProvider(openmct)); | ||||
|             openmct.legacyExtension('policies', {category: 'composition', | ||||
|                 implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct']}); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return plugin; | ||||
| }); | ||||
							
								
								
									
										11
									
								
								src/plugins/summaryWidget/res/conditionTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/plugins/summaryWidget/res/conditionTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <li class="t-condition"> | ||||
|     <label class="t-condition-context">when</label> | ||||
|     <span class="controls"> | ||||
|         <span class="t-configuration"> </span> | ||||
|         <span class="t-value-inputs"> </span> | ||||
|     </span> | ||||
|     <span class="flex-elem l-condition-action-buttons-wrapper"> | ||||
|         <a class="s-icon-button icon-duplicate t-duplicate" title="Duplicate this condition"></a> | ||||
|         <a class="s-icon-button icon-trash t-delete" title="Delete this condition"></a> | ||||
|     </span> | ||||
| </li> | ||||
							
								
								
									
										10
									
								
								src/plugins/summaryWidget/res/input/paletteTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/plugins/summaryWidget/res/input/paletteTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| <a class="e-control s-button s-menu-button menu-element"> | ||||
|    <span class="l-click-area"></span> | ||||
|    <span class="t-swatch"></span> | ||||
|    <div class="menu l-palette"> | ||||
|        <div class="l-palette-row l-option-row"> | ||||
|            <div class="l-palette-item s-palette-item no-selection"></div> | ||||
|            <span class="l-palette-item-label">None</span> | ||||
|        </div> | ||||
|    </div> | ||||
| </a> | ||||
							
								
								
									
										4
									
								
								src/plugins/summaryWidget/res/input/selectTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/plugins/summaryWidget/res/input/selectTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| <div class="e-control select"> | ||||
|     <select> | ||||
|     </select> | ||||
|  </div> | ||||
							
								
								
									
										3
									
								
								src/plugins/summaryWidget/res/ruleImageTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/plugins/summaryWidget/res/ruleImageTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| <div class="holder widget-rules-wrapper"> | ||||
|     <div class="t-drag-rule-image l-widget-rule s-widget-rule"></div> | ||||
| </div> | ||||
							
								
								
									
										73
									
								
								src/plugins/summaryWidget/res/ruleTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/plugins/summaryWidget/res/ruleTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| <div> | ||||
|     <div class="l-widget-rule s-widget-rule l-compact-form"> | ||||
|         <div class="widget-rule-header"> | ||||
|             <span class="flex-elem l-widget-thumb-wrapper"> | ||||
|                 <span class="grippy-holder"> | ||||
|                     <span class="t-grippy grippy"></span> | ||||
|                 </span> | ||||
|                 <span class="view-control expanded"></span> | ||||
|                 <span class="t-widget-thumb widget-thumb"> | ||||
|                     <span class="widget-label">DEF</span> | ||||
|                 </span> | ||||
|             </span> | ||||
|             <span class="flex-elem rule-title">Default Title</span> | ||||
|             <span class="flex-elem rule-description grows">Rule description goes here</span> | ||||
|             <span class="flex-elem l-rule-action-buttons-wrapper"> | ||||
|                 <a class="s-icon-button icon-duplicate t-duplicate" title="Duplicate this rule"></a> | ||||
|                 <a class="s-icon-button icon-trash t-delete" title="Delete this rule"></a> | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="widget-rule-content expanded"> | ||||
|             <ul> | ||||
|                 <li> | ||||
|                     <label>Rule Name:</label> | ||||
|                     <span class="controls"> | ||||
|                         <input class="t-rule-name-input" type="text" /> | ||||
|                     </span> | ||||
|                 </li> | ||||
|                 <li class="connects-to-previous"> | ||||
|                     <label>Label:</label> | ||||
|                     <span class="controls t-label-input"> | ||||
|                         <input class="e-control t-rule-label-input" type="text" /> | ||||
|                     </span> | ||||
|                 </li> | ||||
|                 <li class="connects-to-previous"> | ||||
|                     <label>Message:</label> | ||||
|                     <span class="controls"> | ||||
|                         <input type="text" class="lg s t-rule-message-input" | ||||
|                          placeholder="Will appear as tooltip when hovering on the widget"/> | ||||
|                     </span> | ||||
|                 </li> | ||||
|                 <li class="connects-to-previous"> | ||||
|                     <label>Style:</label> | ||||
|                     <span class="controls t-style-input"> | ||||
|                     </span> | ||||
|                 </li> | ||||
|             </ul> | ||||
|             <ul class="t-widget-rule-config"> | ||||
|                 <li> | ||||
|                     <label>Trigger when</label> | ||||
|                     <span class="controls"> | ||||
|                         <div class="e-control select"> | ||||
|                             <select class="t-trigger"> | ||||
|                                 <option value="any">any condition is met</option> | ||||
|                                 <option value="all">all conditions are met</option> | ||||
|                                 <!-- <option value="js">the following JavaScript evaluates to true</option> --> | ||||
|                             </select> | ||||
|                          </div> | ||||
|                     </span> | ||||
|                 </li> | ||||
|                 <!-- <li class="t-rule-js-condition-input-holder"> | ||||
|                     <textarea placeholder="" class="med t-rule-js-condition-input"></textarea> | ||||
|                 </li> --> | ||||
|                 <li> | ||||
|                     <label></label> | ||||
|                     <span class="controls"> | ||||
|                         <a class="e-control s-button labeled add-condition icon-plus">Add Condition</a> | ||||
|                     </span> | ||||
|                 </li> | ||||
|             </ul> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="t-drag-indicator l-widget-rule s-widget-rule" style="opacity:0;" hidden></div> | ||||
| </div> | ||||
							
								
								
									
										16
									
								
								src/plugins/summaryWidget/res/testDataItemTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/plugins/summaryWidget/res/testDataItemTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <div class="t-test-data-item l-compact-form l-widget-test-data-item s-widget-test-data-item"> | ||||
|     <ul> | ||||
|         <li> | ||||
|             <label>Set </label> | ||||
|             <span class="controls"> | ||||
|                 <span class="t-configuration"></span> | ||||
|                 <span class="equal-to hidden"> equal to </span> | ||||
|                 <span class="t-value-inputs"></span> | ||||
|             </span> | ||||
|             <span class="flex-elem l-widget-test-data-item-action-buttons-wrapper"> | ||||
|                 <a class="s-icon-button icon-duplicate t-duplicate" title="Duplicate this test value"></a> | ||||
|                 <a class="s-icon-button icon-trash t-delete" title="Delete this test value"></a> | ||||
|             </span> | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
							
								
								
									
										15
									
								
								src/plugins/summaryWidget/res/testDataTemplate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/plugins/summaryWidget/res/testDataTemplate.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <div class="flex-accordion-holder"> | ||||
|     <div class="flex-accordion-holder t-widget-test-data-content w-widget-test-data-content"> | ||||
|         <div class="l-enable"> | ||||
|             <label class="checkbox custom">Apply Test Values | ||||
|                 <input type="checkbox" class="t-test-data-checkbox"> | ||||
|                 <em></em> | ||||
|             </label> | ||||
|         </div> | ||||
|         <div class="t-test-data-config w-widget-test-data-items"> | ||||
|             <div class="holder add-rule-button-wrapper align-right"> | ||||
|                 <a id="addRule" class="e-control s-button major labeled add-test-condition icon-plus">Add Test Value</a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										23
									
								
								src/plugins/summaryWidget/res/widgetTemplate.html
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/summaryWidget/res/widgetTemplate.html
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <div class="w-summary-widget s-status-no-data"> | ||||
|     <a id="widget" class="t-summary-widget l-summary-widget s-summary-widget labeled"> | ||||
|         <span id="widgetLabel" class="label widget-label">Default Static Name</span> | ||||
|     </a> | ||||
|     <div class="holder flex-elem t-message-inline l-message message-severity-alert t-message-widget-no-data"> | ||||
|         <div class="w-message-contents l-message-body-only"> | ||||
|             <div class="message-body"> | ||||
|                 You must add at least one telemetry object to edit this widget. | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="holder l-flex-col l-flex-accordion flex-elem grows widget-edit-holder expanded-widget-test-data expanded-widget-rules"> | ||||
|         <div class="section-header"><span class="view-control t-view-control-test-data expanded"></span>Test Data Values</div> | ||||
|         <div class="widget-test-data flex-accordion-holder"></div> | ||||
|         <div class="section-header"><span class="view-control t-view-control-rules expanded"></span>Rules</div> | ||||
|         <div class="holder widget-rules-wrapper flex-elem expanded"> | ||||
|             <div id="ruleArea" class="widget-rules"></div> | ||||
|             <div class="holder add-rule-button-wrapper align-right"> | ||||
|                 <a id="addRule" class="s-button major labeled add-rule-button icon-plus">Add Rule</a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										192
									
								
								src/plugins/summaryWidget/src/Condition.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/plugins/summaryWidget/src/Condition.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| define([ | ||||
|     'text!../res/conditionTemplate.html', | ||||
|     './input/ObjectSelect', | ||||
|     './input/KeySelect', | ||||
|     './input/OperationSelect', | ||||
|     'EventEmitter', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     conditionTemplate, | ||||
|     ObjectSelect, | ||||
|     KeySelect, | ||||
|     OperationSelect, | ||||
|     EventEmitter, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Represents an individual condition for a summary widget rule. Manages the | ||||
|      * associated inputs and view. | ||||
|      * @param {Object} conditionConfig The configurration for this condition, consisting | ||||
|      *                                of object, key, operation, and values fields | ||||
|      * @param {number} index the index of this Condition object in it's parent Rule's data model, | ||||
|      *                        to be injected into callbacks for removes | ||||
|      * @param {ConditionManager} conditionManager A ConditionManager instance for populating | ||||
|      *                                            selects with configuration data | ||||
|      */ | ||||
|     function Condition(conditionConfig, index, conditionManager) { | ||||
|         this.config = conditionConfig; | ||||
|         this.index = index; | ||||
|         this.conditionManager = conditionManager; | ||||
|  | ||||
|         this.domElement = $(conditionTemplate); | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['remove', 'duplicate', 'change']; | ||||
|  | ||||
|         this.deleteButton = $('.t-delete', this.domElement); | ||||
|         this.duplicateButton = $('.t-duplicate', this.domElement); | ||||
|  | ||||
|         this.selects = {}; | ||||
|         this.valueInputs = []; | ||||
|  | ||||
|         this.remove = this.remove.bind(this); | ||||
|         this.duplicate = this.duplicate.bind(this); | ||||
|  | ||||
|         var self = this; | ||||
|  | ||||
|         /** | ||||
|          * Event handler for a change in one of this conditions' custom selects | ||||
|          * @param {string} value The new value of this selects | ||||
|          * @param {string} property The property of this condition to modify | ||||
|          * @private | ||||
|          */ | ||||
|         function onSelectChange(value, property) { | ||||
|             if (property === 'operation') { | ||||
|                 self.generateValueInputs(value); | ||||
|             } | ||||
|             self.eventEmitter.emit('change', { | ||||
|                 value: value, | ||||
|                 property: property, | ||||
|                 index: self.index | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Event handler for this conditions value inputs | ||||
|          * @param {Event} event The oninput event that triggered this callback | ||||
|          * @private | ||||
|          */ | ||||
|         function onValueInput(event) { | ||||
|             var elem = event.target, | ||||
|                 value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber), | ||||
|                 inputIndex = self.valueInputs.indexOf(elem); | ||||
|  | ||||
|             self.eventEmitter.emit('change', { | ||||
|                 value: value, | ||||
|                 property: 'values[' + inputIndex + ']', | ||||
|                 index: self.index | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.deleteButton.on('click', this.remove); | ||||
|         this.duplicateButton.on('click', this.duplicate); | ||||
|  | ||||
|         this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ | ||||
|             ['any', 'any telemetry'], | ||||
|             ['all', 'all telemetry'] | ||||
|         ]); | ||||
|         this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager); | ||||
|         this.selects.operation = new OperationSelect( | ||||
|             this.config, | ||||
|             this.selects.key, | ||||
|             this.conditionManager, | ||||
|             function (value) { | ||||
|                 onSelectChange(value, 'operation'); | ||||
|             }); | ||||
|  | ||||
|         this.selects.object.on('change', function (value) { | ||||
|             onSelectChange(value, 'object'); | ||||
|         }); | ||||
|         this.selects.key.on('change', function (value) { | ||||
|             onSelectChange(value, 'key'); | ||||
|         }); | ||||
|  | ||||
|         Object.values(this.selects).forEach(function (select) { | ||||
|             $('.t-configuration', self.domElement).append(select.getDOM()); | ||||
|         }); | ||||
|  | ||||
|         $(this.domElement).on('input', 'input', onValueInput); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the DOM element representing this condition in the view | ||||
|      * @return {Element} | ||||
|      */ | ||||
|     Condition.prototype.getDOM = function (container) { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this condition: supported callbacks are remove, change, | ||||
|      * duplicate | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     Condition.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Hide the appropriate inputs when this is the only condition | ||||
|      */ | ||||
|     Condition.prototype.hideButtons = function () { | ||||
|         this.deleteButton.hide(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Remove this condition from the configuration. Invokes any registered | ||||
|      * remove callbacks | ||||
|      */ | ||||
|     Condition.prototype.remove = function () { | ||||
|         this.eventEmitter.emit('remove', this.index); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Make a deep clone of this condition's configuration and invoke any duplicate | ||||
|      * callbacks with the cloned configuration and this rule's index | ||||
|      */ | ||||
|     Condition.prototype.duplicate = function () { | ||||
|         var sourceCondition = JSON.parse(JSON.stringify(this.config)); | ||||
|         this.eventEmitter.emit('duplicate', { | ||||
|             sourceCondition: sourceCondition, | ||||
|             index: this.index | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * When an operation is selected, create the appropriate value inputs | ||||
|      * and add them to the view | ||||
|      * @param {string} operation The key of currently selected operation | ||||
|      */ | ||||
|     Condition.prototype.generateValueInputs = function (operation) { | ||||
|         var evaluator = this.conditionManager.getEvaluator(), | ||||
|             inputArea = $('.t-value-inputs', this.domElement), | ||||
|             inputCount, | ||||
|             inputType, | ||||
|             newInput, | ||||
|             index = 0; | ||||
|  | ||||
|         inputArea.html(''); | ||||
|         this.valueInputs = []; | ||||
|  | ||||
|         if (evaluator.getInputCount(operation)) { | ||||
|             inputCount = evaluator.getInputCount(operation); | ||||
|             inputType = evaluator.getInputType(operation); | ||||
|             while (index < inputCount) { | ||||
|                 if (!this.config.values[index]) { | ||||
|                     this.config.values[index] = (inputType === 'number' ? 0 : ''); | ||||
|                 } | ||||
|                 newInput = $('<input class="sm" type = "' + inputType + '" value = "' + this.config.values[index] + '"> </input>'); | ||||
|                 this.valueInputs.push(newInput.get(0)); | ||||
|                 inputArea.append(newInput); | ||||
|                 index += 1; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return Condition; | ||||
| }); | ||||
							
								
								
									
										449
									
								
								src/plugins/summaryWidget/src/ConditionEvaluator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										449
									
								
								src/plugins/summaryWidget/src/ConditionEvaluator.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,449 @@ | ||||
| define([], function () { | ||||
|  | ||||
|     /** | ||||
|      * Responsible for maintaining the possible operations for conditions | ||||
|      * in this widget, and evaluating the boolean value of conditions passed as | ||||
|      * input. | ||||
|      * @constructor | ||||
|      * @param {Object} subscriptionCache A cache consisting of the latest available | ||||
|      *                                   data for any telemetry sources in the widget's | ||||
|      *                                   composition. | ||||
|      * @param {Object} compositionObjs The current set of composition objects to | ||||
|      *                                 evaluate for 'any' and 'all' conditions | ||||
|      */ | ||||
|     function ConditionEvaluator(subscriptionCache, compositionObjs) { | ||||
|         this.subscriptionCache = subscriptionCache; | ||||
|         this.compositionObjs = compositionObjs; | ||||
|  | ||||
|         this.testCache = {}; | ||||
|         this.useTestCache = false; | ||||
|  | ||||
|         /** | ||||
|          * Maps value types to HTML input field types. These | ||||
|          * type of inputs will be generated by conditions expecting this data type | ||||
|          */ | ||||
|         this.inputTypes = { | ||||
|             number: 'number', | ||||
|             string: 'text' | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Functions to validate that the input to an operation is of the type | ||||
|          * that it expects, in order to prevent unexpected behavior. Will be | ||||
|          * invoked before the corresponding operation is executed | ||||
|          */ | ||||
|         this.inputValidators = { | ||||
|             number: this.validateNumberInput, | ||||
|             string: this.validateStringInput | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * A library of operations supported by this rule evaluator. Each operation | ||||
|          * consists of the following fields: | ||||
|          * operation: a function with boolean return type to be invoked when this | ||||
|          *            operation is used. Will be called with an array of inputs | ||||
|          *            where input [0] is the telemetry value and input [1..n] are | ||||
|          *            any comparison values | ||||
|          * text: a human-readable description of this operation to populate selects | ||||
|          * appliesTo: an array of identifiers for types that operation may be used on | ||||
|          * inputCount: the number of inputs required to get any necessary comparison | ||||
|          *             values for the operation | ||||
|          * getDescription: A function returning a human-readable shorthand description of | ||||
|          *                this operation to populate the 'description' field in the rule header. | ||||
|          *                Will be invoked with an array of a condition's comparison values. | ||||
|          */ | ||||
|         this.operations = { | ||||
|             equalTo: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] === input[1]; | ||||
|                 }, | ||||
|                 text: 'is equal to', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' == ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             notEqualTo: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] !== input[1]; | ||||
|                 }, | ||||
|                 text: 'is not equal to', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' != ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             greaterThan: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] > input[1]; | ||||
|                 }, | ||||
|                 text: 'is greater than', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' > ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             lessThan: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] < input[1]; | ||||
|                 }, | ||||
|                 text: 'is less than', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' < ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             greaterThanOrEq: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] >= input[1]; | ||||
|                 }, | ||||
|                 text: 'is greater than or equal to', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' >= ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             lessThanOrEq: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] <= input[1]; | ||||
|                 }, | ||||
|                 text: 'is less than or equal to', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' <= ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             between: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] > input[1] && input[0] < input[2]; | ||||
|                 }, | ||||
|                 text: 'is between', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 2, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' between ' + values[0] + ' and ' + values[1]; | ||||
|                 } | ||||
|             }, | ||||
|             notBetween: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] < input[1] || input[0] > input[2]; | ||||
|                 }, | ||||
|                 text: 'is not between', | ||||
|                 appliesTo: ['number'], | ||||
|                 inputCount: 2, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' not between ' + values[0] + ' and ' + values[1]; | ||||
|                 } | ||||
|             }, | ||||
|             textContains: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] && input[1] && input[0].includes(input[1]); | ||||
|                 }, | ||||
|                 text: 'text contains', | ||||
|                 appliesTo: ['string'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' contains ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             textDoesNotContain: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] && input[1] && !input[0].includes(input[1]); | ||||
|                 }, | ||||
|                 text: 'text does not contain', | ||||
|                 appliesTo: ['string'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' does not contain ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             textStartsWith: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0].startsWith(input[1]); | ||||
|                 }, | ||||
|                 text: 'text starts with', | ||||
|                 appliesTo: ['string'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' starts with ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             textEndsWith: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0].endsWith(input[1]); | ||||
|                 }, | ||||
|                 text: 'text ends with', | ||||
|                 appliesTo: ['string'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' ends with ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             textIsExactly: { | ||||
|                 operation: function (input) { | ||||
|                     return input[0] === input[1]; | ||||
|                 }, | ||||
|                 text: 'text is exactly', | ||||
|                 appliesTo: ['string'], | ||||
|                 inputCount: 1, | ||||
|                 getDescription: function (values) { | ||||
|                     return ' is exactly ' + values[0]; | ||||
|                 } | ||||
|             }, | ||||
|             isUndefined: { | ||||
|                 operation: function (input) { | ||||
|                     return typeof input[0] === 'undefined'; | ||||
|                 }, | ||||
|                 text: 'is undefined', | ||||
|                 appliesTo: ['string', 'number'], | ||||
|                 inputCount: 0, | ||||
|                 getDescription: function () { | ||||
|                     return ' is undefined'; | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Evaluate the conditions passed in as an argument, and return the boolean | ||||
|      * value of these conditions. Available evaluation modes are 'any', which will | ||||
|      * return true if any of the conditions evaluates to true (i.e. logical OR); 'all', | ||||
|      * which returns true only if all conditions evalute to true (i.e. logical AND); | ||||
|      * or 'js', which returns the boolean value of a custom JavaScript conditional. | ||||
|      * @param {} conditions Either an array of objects with object, key, operation, | ||||
|      *                      and value fields, or a string representing a JavaScript | ||||
|      *                      condition. | ||||
|      * @param {string} mode The key of the mode to use when evaluating the conditions. | ||||
|      * @return {boolean} The boolean value of the conditions | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.execute = function (conditions, mode) { | ||||
|         var active = false, | ||||
|             conditionValue, | ||||
|             conditionDefined = false, | ||||
|             self = this, | ||||
|             firstRuleEvaluated = false, | ||||
|             compositionObjs = this.compositionObjs; | ||||
|  | ||||
|         if (mode === 'js') { | ||||
|             active = this.executeJavaScriptCondition(conditions); | ||||
|         } else { | ||||
|             (conditions || []).forEach(function (condition) { | ||||
|                 conditionDefined = false; | ||||
|                 if (condition.object === 'any') { | ||||
|                     conditionValue = false; | ||||
|                     Object.keys(compositionObjs).forEach(function (objId) { | ||||
|                         try { | ||||
|                             conditionValue = conditionValue || | ||||
|                                 self.executeCondition(objId, condition.key, | ||||
|                                     condition.operation, condition.values); | ||||
|                             conditionDefined = true; | ||||
|                         } catch (e) { | ||||
|                             //ignore a malformed condition | ||||
|                         } | ||||
|                     }); | ||||
|                 } else if (condition.object === 'all') { | ||||
|                     conditionValue = true; | ||||
|                     Object.keys(compositionObjs).forEach(function (objId) { | ||||
|                         try { | ||||
|                             conditionValue = conditionValue && | ||||
|                                 self.executeCondition(objId, condition.key, | ||||
|                                     condition.operation, condition.values); | ||||
|                             conditionDefined = true; | ||||
|                         } catch (e) { | ||||
|                             //ignore a malformed condition | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     try { | ||||
|                         conditionValue = self.executeCondition(condition.object, condition.key, | ||||
|                             condition.operation, condition.values); | ||||
|                         conditionDefined = true; | ||||
|                     } catch (e) { | ||||
|                         //ignore malformed condition | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (conditionDefined) { | ||||
|                     active = (mode === 'all' && !firstRuleEvaluated ? true : active); | ||||
|                     firstRuleEvaluated = true; | ||||
|                     if (mode === 'any') { | ||||
|                         active = active || conditionValue; | ||||
|                     } else if (mode === 'all') { | ||||
|                         active = active && conditionValue; | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         return active; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Execute a condition defined as an object. | ||||
|      * @param {string} object The identifier of the telemetry object to retrieve data from | ||||
|      * @param {string} key The property of the telemetry object | ||||
|      * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition | ||||
|      * @param {string} values An array of comparison values to invoke the operation with | ||||
|      * @return {boolean} The value of this condition | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) { | ||||
|         var cache = (this.useTestCache ? this.testCache : this.subscriptionCache), | ||||
|             telemetryValue, | ||||
|             op, | ||||
|             input, | ||||
|             validator; | ||||
|  | ||||
|         if (cache[object] && typeof cache[object][key] !== 'undefined') { | ||||
|             telemetryValue = [cache[object][key]]; | ||||
|         } | ||||
|         op = this.operations[operation] && this.operations[operation].operation; | ||||
|         input = telemetryValue && telemetryValue.concat(values); | ||||
|         validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; | ||||
|  | ||||
|         if (op && input && validator) { | ||||
|             return validator(input) && op(input); | ||||
|         } else { | ||||
|             throw new Error('Malformed condition'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Interpret a string as a JavaScript conditional, and return its boolean value | ||||
|      * @param {string} condition The string to interpreted as JavaScript | ||||
|      * @return {boolean} The value of the conditions | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.executeJavaScriptCondition = function (condition) { | ||||
|         var conditionValue = false; | ||||
|         //TODO: implement JavaScript execution | ||||
|         return conditionValue; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A function that returns true only if each value in its input argument is | ||||
|      * of a numerical type | ||||
|      * @param {[]} input An array of values | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.validateNumberInput = function (input) { | ||||
|         var valid = true; | ||||
|         input.forEach(function (value) { | ||||
|             valid = valid && (typeof value === 'number'); | ||||
|         }); | ||||
|         return valid; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A function that returns true only if each value in its input argument is | ||||
|      * a string | ||||
|      * @param {[]} input An array of values | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.validateStringInput = function (input) { | ||||
|         var valid = true; | ||||
|         input.forEach(function (value) { | ||||
|             valid = valid && (typeof value === 'string'); | ||||
|         }); | ||||
|         return valid; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the keys of operations supported by this evaluator | ||||
|      * @return {string[]} An array of the keys of supported operations | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getOperationKeys = function () { | ||||
|         return Object.keys(this.operations); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the human-readable text corresponding to a given operation | ||||
|      * @param {string} key The key of the operation | ||||
|      * @return {string} The text description of the operation | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getOperationText = function (key) { | ||||
|         return this.operations[key].text; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns true only of the given operation applies to a given type | ||||
|      * @param {string} key The key of the operation | ||||
|      * @param {string} type The value type to query | ||||
|      * @returns {boolean} True if the condition applies, false otherwise | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { | ||||
|         return (this.operations[key].appliesTo.includes(type)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return the number of value inputs required by an operation | ||||
|      * @param {string} key The key of the operation to query | ||||
|      * @return {number} | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getInputCount = function (key) { | ||||
|         if (this.operations[key]) { | ||||
|             return this.operations[key].inputCount; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return the human-readable shorthand description of the operation for a rule header | ||||
|      * @param {string} key The key of the operation to query | ||||
|      * @param {} values An array of values with which to invoke the getDescription function | ||||
|      *                  of the operation | ||||
|      * @return {string} A text description of this operation | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getOperationDescription = function (key, values) { | ||||
|         if (this.operations[key]) { | ||||
|             return this.operations[key].getDescription(values); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return the HTML input type associated with a given operation | ||||
|      * @param {string} key The key of the operation to query | ||||
|      * @return {string} The key for an HTML5 input type | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getInputType = function (key) { | ||||
|         var type; | ||||
|         if (this.operations[key]) { | ||||
|             type = this.operations[key].appliesTo[0]; | ||||
|         } | ||||
|         if (this.inputTypes[type]) { | ||||
|             return this.inputTypes[type]; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the HTML input type associated with a value type | ||||
|      * @param {string} dataType The JavaScript value type | ||||
|      * @return {string} The key for an HTML5 input type | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.getInputTypeById = function (dataType) { | ||||
|         return this.inputTypes[dataType]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Set the test data cache used by this rule evaluator | ||||
|      * @param {object} testCache A mock cache following the format of the real | ||||
|      *                           subscription cache | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.setTestDataCache = function (testCache) { | ||||
|         this.testCache = testCache; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Have this RuleEvaluator pull data values from the provided test cache | ||||
|      * instead of its actual subscription cache when evaluating. If invoked with true, | ||||
|      * will use the test cache; otherwise, will use the subscription cache | ||||
|      * @param {boolean} useTestData Boolean flag | ||||
|      */ | ||||
|     ConditionEvaluator.prototype.useTestData = function (useTestCache) { | ||||
|         this.useTestCache = useTestCache; | ||||
|     }; | ||||
|  | ||||
|     return ConditionEvaluator; | ||||
| }); | ||||
							
								
								
									
										372
									
								
								src/plugins/summaryWidget/src/ConditionManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								src/plugins/summaryWidget/src/ConditionManager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,372 @@ | ||||
| define ([ | ||||
|     './ConditionEvaluator', | ||||
|     'EventEmitter', | ||||
|     'zepto', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     ConditionEvaluator, | ||||
|     EventEmitter, | ||||
|     $, | ||||
|     _ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Provides a centralized content manager for conditions in the summary widget. | ||||
|      * Loads and caches composition and telemetry subscriptions, and maintains a | ||||
|      * {ConditionEvaluator} instance to handle evaluation | ||||
|      * @constructor | ||||
|      * @param {Object} domainObject the Summary Widget domain object | ||||
|      * @param {MCT} openmct an MCT instance | ||||
|      */ | ||||
|     function ConditionManager(domainObject, openmct) { | ||||
|         this.domainObject = domainObject; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.compositionObjs = {}; | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry']; | ||||
|  | ||||
|         this.keywordLabels = { | ||||
|             any: 'any Telemetry', | ||||
|             all: 'all Telemetry' | ||||
|         }; | ||||
|  | ||||
|         this.telemetryMetadataById = { | ||||
|             any: {}, | ||||
|             all: {} | ||||
|         }; | ||||
|  | ||||
|         this.telemetryTypesById = { | ||||
|             any: {}, | ||||
|             all: {} | ||||
|         }; | ||||
|  | ||||
|         this.subscriptions = {}; | ||||
|         this.subscriptionCache = {}; | ||||
|         this.loadComplete = false; | ||||
|         this.metadataLoadComplete = false; | ||||
|         this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs); | ||||
|  | ||||
|         this.composition.on('add', this.onCompositionAdd, this); | ||||
|         this.composition.on('remove', this.onCompositionRemove, this); | ||||
|         this.composition.on('load', this.onCompositionLoad, this); | ||||
|  | ||||
|         this.composition.load(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this ConditionManager: supported callbacks are add | ||||
|      * remove, load, metadata, and receiveTelemetry | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     ConditionManager.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Given a set of rules, execute the conditions associated with each rule | ||||
|      * and return the id of the last rule whose conditions evaluate to true | ||||
|      * @param {string[]} ruleOrder An array of rule IDs indicating what order They | ||||
|      *                             should be evaluated in | ||||
|      * @param {Object} rules An object mapping rule IDs to rule configurations | ||||
|      * @return {string} The ID of the rule to display on the widget | ||||
|      */ | ||||
|     ConditionManager.prototype.executeRules = function (ruleOrder, rules) { | ||||
|         var self = this, | ||||
|             activeId = ruleOrder[0], | ||||
|             rule, | ||||
|             conditions; | ||||
|  | ||||
|         ruleOrder.forEach(function (ruleId) { | ||||
|             rule = rules[ruleId]; | ||||
|             conditions = rule.getProperty('trigger') === 'js' ? | ||||
|                 rule.getProperty('jsCondition') : rule.getProperty('conditions'); | ||||
|             if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { | ||||
|                 activeId = ruleId; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return activeId; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Adds a field to the list of all available metadata fields in the widget | ||||
|      * @param {Object} metadatum An object representing a set of telemetry metadata | ||||
|      */ | ||||
|     ConditionManager.prototype.addGlobalMetadata = function (metadatum) { | ||||
|         this.telemetryMetadataById.any[metadatum.key] = metadatum; | ||||
|         this.telemetryMetadataById.all[metadatum.key] = metadatum; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Adds a field to the list of properties for globally available metadata | ||||
|      * @param {string} key The key for the property this type applies to | ||||
|      * @param {string} type The type that should be associated with this property | ||||
|      */ | ||||
|     ConditionManager.prototype.addGlobalPropertyType = function (key, type) { | ||||
|         this.telemetryTypesById.any[key] = type; | ||||
|         this.telemetryTypesById.all[key] = type; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Given a telemetry-producing domain object, associate each of it's telemetry | ||||
|      * fields with a type, parsing from historical data. | ||||
|      * @param {Object} object a domain object that can produce telemetry | ||||
|      * @return {Promise} A promise that resolves when a telemetry request | ||||
|      *                   has completed and types have been parsed | ||||
|      */ | ||||
|     ConditionManager.prototype.parsePropertyTypes = function (object) { | ||||
|         var telemetryAPI = this.openmct.telemetry, | ||||
|             key, | ||||
|             type, | ||||
|             self = this; | ||||
|  | ||||
|         self.telemetryTypesById[object.identifier.key] = {}; | ||||
|         return telemetryAPI.request(object, {}).then(function (telemetry) { | ||||
|             Object.entries(telemetry[telemetry.length - 1]).forEach(function (telem) { | ||||
|                 key = telem[0]; | ||||
|                 type = typeof telem[1]; | ||||
|                 self.telemetryTypesById[object.identifier.key][key] = type; | ||||
|                 self.subscriptionCache[object.identifier.key][key] = telem[1]; | ||||
|                 self.addGlobalPropertyType(key, type); | ||||
|             }); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Parse types of telemetry fields from all composition objects; used internally | ||||
|      * to perform a block types load once initial composition load has completed | ||||
|      * @return {Promise} A promise that resolves when all metadata has been loaded | ||||
|      *                   and property types parsed | ||||
|      */ | ||||
|     ConditionManager.prototype.parseAllPropertyTypes = function () { | ||||
|         var self = this, | ||||
|             index = 0, | ||||
|             objs = Object.values(self.compositionObjs), | ||||
|             promise = new Promise(function (resolve, reject) { | ||||
|                 if (objs.length === 0) { | ||||
|                     resolve(); | ||||
|                 } | ||||
|                 objs.forEach(function (obj) { | ||||
|                     self.parsePropertyTypes(obj).then(function () { | ||||
|                         if (index === objs.length - 1) { | ||||
|                             resolve(); | ||||
|                         } | ||||
|                         index += 1; | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         return promise; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Invoked when a telemtry subscription yields new data. Updates the LAD | ||||
|      * cache and invokes any registered receiveTelemetry callbacks | ||||
|      * @param {string} objId The key associated with the telemetry source | ||||
|      * @param {datum} datum The new data from the telemetry source | ||||
|      * @private | ||||
|      */ | ||||
|     ConditionManager.prototype.handleSubscriptionCallback = function (objId, datum) { | ||||
|         this.subscriptionCache[objId] = datum; | ||||
|         this.eventEmitter.emit('receiveTelemetry'); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Event handler for an add event in this Summary Widget's composition. | ||||
|      * Sets up subscription handlers and parses its property types. | ||||
|      * @param {Object} obj The newly added domain object | ||||
|      * @private | ||||
|      */ | ||||
|     ConditionManager.prototype.onCompositionAdd = function (obj) { | ||||
|         var compositionKeys, | ||||
|             telemetryAPI = this.openmct.telemetry, | ||||
|             objId = obj.identifier.key, | ||||
|             telemetryMetadata, | ||||
|             self = this; | ||||
|  | ||||
|         if (telemetryAPI.canProvideTelemetry(obj)) { | ||||
|             self.compositionObjs[objId] = obj; | ||||
|             self.telemetryMetadataById[objId] = {}; | ||||
|  | ||||
|             compositionKeys = self.domainObject.composition.map(function (object) { | ||||
|                 return object.key; | ||||
|             }); | ||||
|             if (!compositionKeys.includes(obj.identifier.key)) { | ||||
|                 self.domainObject.composition.push(obj.identifier); | ||||
|             } | ||||
|  | ||||
|             telemetryMetadata = telemetryAPI.getMetadata(obj).values(); | ||||
|             telemetryMetadata.forEach(function (metaDatum) { | ||||
|                 self.telemetryMetadataById[objId][metaDatum.key] = metaDatum; | ||||
|                 self.addGlobalMetadata(metaDatum); | ||||
|             }); | ||||
|  | ||||
|             self.subscriptionCache[objId] = {}; | ||||
|             self.subscriptions[objId] = telemetryAPI.subscribe(obj, function (datum) { | ||||
|                 self.handleSubscriptionCallback(objId, datum); | ||||
|             }, {}); | ||||
|  | ||||
|             /** | ||||
|              * if this is the initial load, parsing property types will be postponed | ||||
|              * until all composition objects have been loaded | ||||
|              */ | ||||
|             if (self.loadComplete) { | ||||
|                 self.parsePropertyTypes(obj); | ||||
|             } | ||||
|  | ||||
|             self.eventEmitter.emit('add', obj); | ||||
|  | ||||
|             $('.w-summary-widget').removeClass('s-status-no-data'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Invoked on a remove event in this Summary Widget's compostion. Removes | ||||
|      * the object from the local composition, and untracks it | ||||
|      * @param {object} identifier The identifier of the object to be removed | ||||
|      * @private | ||||
|      */ | ||||
|     ConditionManager.prototype.onCompositionRemove = function (identifier) { | ||||
|         _.remove(this.domainObject.composition, function (id) { | ||||
|             return id.key === identifier.key; | ||||
|         }); | ||||
|         delete this.compositionObjs[identifier.key]; | ||||
|         this.subscriptions[identifier.key](); //unsubscribe from telemetry source | ||||
|         this.eventEmitter.emit('remove', identifier); | ||||
|  | ||||
|         if (_.isEmpty(this.compositionObjs)) { | ||||
|             $('.w-summary-widget').addClass('s-status-no-data'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Invoked when the Summary Widget's composition finishes its initial load. | ||||
|      * Invokes any registered load callbacks, does a block load of all metadata, | ||||
|      * and then invokes any registered metadata load callbacks. | ||||
|      * @private | ||||
|      */ | ||||
|     ConditionManager.prototype.onCompositionLoad = function () { | ||||
|         var self = this; | ||||
|         self.loadComplete = true; | ||||
|         self.eventEmitter.emit('load'); | ||||
|         self.parseAllPropertyTypes().then(function () { | ||||
|             self.metadataLoadComplete = true; | ||||
|             self.eventEmitter.emit('metadata'); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the currently tracked telemetry sources | ||||
|      * @return {Object} An object mapping object keys to domain objects | ||||
|      */ | ||||
|     ConditionManager.prototype.getComposition = function () { | ||||
|         return this.compositionObjs; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the human-readable name of a domain object from its key | ||||
|      * @param {string} id The key of the domain object | ||||
|      * @return {string} The human-readable name of the domain object | ||||
|      */ | ||||
|     ConditionManager.prototype.getObjectName = function (id) { | ||||
|         var name; | ||||
|  | ||||
|         if (this.keywordLabels[id]) { | ||||
|             name = this.keywordLabels[id]; | ||||
|         } else if (this.compositionObjs[id]) { | ||||
|             name = this.compositionObjs[id].name; | ||||
|         } | ||||
|  | ||||
|         return name; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the property metadata associated with a given telemetry source | ||||
|      * @param {string} id The key associated with the domain object | ||||
|      * @return {Object} Returns an object with fields representing each telemetry field | ||||
|      */ | ||||
|     ConditionManager.prototype.getTelemetryMetadata = function (id) { | ||||
|         return this.telemetryMetadataById[id]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the type associated with a telemtry data field of a particular domain | ||||
|      * object | ||||
|      * @param {string} id The key associated with the domain object | ||||
|      * @param {string} property The telemetry field key to retrieve the type of | ||||
|      * @return {string} The type name | ||||
|      */ | ||||
|     ConditionManager.prototype.getTelemetryPropertyType = function (id, property) { | ||||
|         if (this.telemetryTypesById[id]) { | ||||
|             return this.telemetryTypesById[id][property]; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the human-readable name of a telemtry data field of a particular domain | ||||
|      * object | ||||
|      * @param {string} id The key associated with the domain object | ||||
|      * @param {string} property The telemetry field key to retrieve the type of | ||||
|      * @return {string} The telemetry field name | ||||
|      */ | ||||
|     ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { | ||||
|         if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { | ||||
|             return this.telemetryMetadataById[id][property].name; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the {ConditionEvaluator} instance associated with this condition | ||||
|      * manager | ||||
|      * @return {ConditionEvaluator} | ||||
|      */ | ||||
|     ConditionManager.prototype.getEvaluator = function () { | ||||
|         return this.evaluator; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns true if the initial compostion load has completed | ||||
|      * @return {boolean} | ||||
|      */ | ||||
|     ConditionManager.prototype.loadCompleted = function () { | ||||
|         return this.loadComplete; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns true if the initial block metadata load has completed | ||||
|      */ | ||||
|     ConditionManager.prototype.metadataLoadCompleted = function () { | ||||
|         return this.metadataLoadComplete; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Triggers the telemetryRecieve callbacks registered to this ConditionManager, | ||||
|      * used by the {TestDataManager} to force a rule evaluation when test data is | ||||
|      * enabled | ||||
|      */ | ||||
|     ConditionManager.prototype.triggerTelemetryCallback = function () { | ||||
|         this.eventEmitter.emit('receiveTelemetry'); | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Unsubscribe from all registered telemetry sources and unregister all event | ||||
|      * listeners registered with the Open MCT APIs | ||||
|      */ | ||||
|     ConditionManager.prototype.destroy = function () { | ||||
|         Object.values(this.subscriptions).forEach(function (unsubscribeFunction) { | ||||
|             unsubscribeFunction(); | ||||
|         }); | ||||
|         this.composition.off('add', this.onCompositionAdd, this); | ||||
|         this.composition.off('remove', this.onCompositionRemove, this); | ||||
|         this.composition.off('load', this.onCompositionLoad, this); | ||||
|     }; | ||||
|  | ||||
|     return ConditionManager; | ||||
| }); | ||||
							
								
								
									
										467
									
								
								src/plugins/summaryWidget/src/Rule.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										467
									
								
								src/plugins/summaryWidget/src/Rule.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,467 @@ | ||||
| define([ | ||||
|     'text!../res/ruleTemplate.html', | ||||
|     './Condition', | ||||
|     './input/ColorPalette', | ||||
|     './input/IconPalette', | ||||
|     'EventEmitter', | ||||
|     'lodash', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     ruleTemplate, | ||||
|     Condition, | ||||
|     ColorPalette, | ||||
|     IconPalette, | ||||
|     EventEmitter, | ||||
|     _, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * An object representing a summary widget rule. Maintains a set of text | ||||
|      * and css properties for output, and a set of conditions for configuring | ||||
|      * when the rule will be applied to the summary widget. | ||||
|      * @constructor | ||||
|      * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule | ||||
|      * @param {Object} domainObject The Summary Widget domain object which contains this rule | ||||
|      * @param {MCT} openmct An MCT instance | ||||
|      * @param {ConditionManager} conditionManager A ConditionManager instance | ||||
|      * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules | ||||
|      * @param {element} container The DOM element which cotains this summary widget | ||||
|      */ | ||||
|     function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.config = ruleConfig; | ||||
|         this.domainObject = domainObject; | ||||
|         this.openmct = openmct; | ||||
|         this.conditionManager = conditionManager; | ||||
|         this.widgetDnD = widgetDnD; | ||||
|         this.container = container; | ||||
|  | ||||
|         this.domElement = $(ruleTemplate); | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange']; | ||||
|         this.conditions = []; | ||||
|         this.dragging = false; | ||||
|  | ||||
|         this.remove = this.remove.bind(this); | ||||
|         this.duplicate = this.duplicate.bind(this); | ||||
|  | ||||
|         this.thumbnail = $('.t-widget-thumb', this.domElement); | ||||
|         this.thumbnailLabel = $('.widget-label', this.domElement); | ||||
|         this.title = $('.rule-title', this.domElement); | ||||
|         this.description = $('.rule-description', this.domElement); | ||||
|         this.trigger = $('.t-trigger', this.domElement); | ||||
|         this.toggleConfigButton = $('.view-control', this.domElement); | ||||
|         this.configArea = $('.widget-rule-content', this.domElement); | ||||
|         this.grippy = $('.t-grippy', this.domElement); | ||||
|         this.conditionArea = $('.t-widget-rule-config', this.domElement); | ||||
|         this.jsConditionArea = $('.t-rule-js-condition-input-holder', this.domElement); | ||||
|         this.deleteButton = $('.t-delete', this.domElement); | ||||
|         this.duplicateButton = $('.t-duplicate', this.domElement); | ||||
|         this.addConditionButton = $('.add-condition', this.domElement); | ||||
|  | ||||
|         /** | ||||
|          * The text inputs for this rule: any input included in this object will | ||||
|          * have the appropriate event handlers registered to it, and it's corresponding | ||||
|          * field in the domain object will be updated with its value | ||||
|          */ | ||||
|         this.textInputs = { | ||||
|             name: $('.t-rule-name-input', this.domElement), | ||||
|             label: $('.t-rule-label-input', this.domElement), | ||||
|             message: $('.t-rule-message-input', this.domElement), | ||||
|             jsCondition: $('.t-rule-js-condition-input', this.domElement) | ||||
|         }; | ||||
|  | ||||
|         this.iconInput = new IconPalette('', container); | ||||
|         this.colorInputs = { | ||||
|             'background-color': new ColorPalette('icon-paint-bucket', container), | ||||
|             'border-color': new ColorPalette('icon-line-horz', container), | ||||
|             'color': new ColorPalette('icon-T', container) | ||||
|         }; | ||||
|  | ||||
|         this.colorInputs.color.toggleNullOption(); | ||||
|  | ||||
|         /** | ||||
|          * An onchange event handler method for this rule's icon palettes | ||||
|          * @param {string} icon The css class name corresponding to this icon | ||||
|          * @private | ||||
|          */ | ||||
|         function onIconInput(icon) { | ||||
|             self.config.icon = icon; | ||||
|             self.updateDomainObject('icon', icon); | ||||
|             self.thumbnailLabel.removeClass().addClass('label widget-label ' + icon); | ||||
|             self.eventEmitter.emit('change'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * An onchange event handler method for this rule's color palettes palettes | ||||
|          * @param {string} color The color selected in the palette | ||||
|          * @param {string} property The css property which this color corresponds to | ||||
|          * @private | ||||
|          */ | ||||
|         function onColorInput(color, property) { | ||||
|             self.config.style[property] = color; | ||||
|             self.updateDomainObject(); | ||||
|             self.thumbnail.css(property, color); | ||||
|             self.eventEmitter.emit('change'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * An onchange event handler method for this rule's trigger key | ||||
|          * @param {event} event The change event from this rule's select element | ||||
|          * @private | ||||
|          */ | ||||
|         function onTriggerInput(event) { | ||||
|             var elem = event.target; | ||||
|             self.config.trigger = elem.value; | ||||
|             self.generateDescription(); | ||||
|             self.updateDomainObject(); | ||||
|             self.refreshConditions(); | ||||
|             self.eventEmitter.emit('conditionChange'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * An onchange event handler method for this rule's text inputs | ||||
|          * @param {element} elem The input element that generated the event | ||||
|          * @param {string} inputKey The field of this rule's configuration to update | ||||
|          * @private | ||||
|          */ | ||||
|         function onTextInput(elem, inputKey) { | ||||
|             self.config[inputKey] = elem.value; | ||||
|             self.updateDomainObject(); | ||||
|             if (inputKey === 'name') { | ||||
|                 self.title.html(elem.value); | ||||
|             } else if (inputKey === 'label') { | ||||
|                 self.thumbnailLabel.html(elem.value); | ||||
|             } | ||||
|             self.eventEmitter.emit('change'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * An onchange event handler for a mousedown event that initiates a drag gesture | ||||
|          * @param {event} event A mouseup event that was registered on this rule's grippy | ||||
|          * @private | ||||
|          */ | ||||
|         function onDragStart(event) { | ||||
|             $('.t-drag-indicator').each(function () { | ||||
|                 $(this).html($('.widget-rule-header', self.domElement).clone().get(0)); | ||||
|             }); | ||||
|             self.widgetDnD.setDragImage($('.widget-rule-header', self.domElement).clone().get(0)); | ||||
|             self.widgetDnD.dragStart(self.config.id); | ||||
|             self.domElement.hide(); | ||||
|         } | ||||
|         /** | ||||
|          * Show or hide this rule's configuration properties | ||||
|          * @private | ||||
|          */ | ||||
|         function toggleConfig() { | ||||
|             self.configArea.toggleClass('expanded'); | ||||
|             self.toggleConfigButton.toggleClass('expanded'); | ||||
|             self.config.expanded = !self.config.expanded; | ||||
|         } | ||||
|  | ||||
|         $('.t-rule-label-input', this.domElement).before(this.iconInput.getDOM()); | ||||
|         this.iconInput.set(self.config.icon); | ||||
|         this.iconInput.on('change', function (value) { | ||||
|             onIconInput(value); | ||||
|         }); | ||||
|  | ||||
|         // Initialize thumbs when first loading | ||||
|         this.thumbnailLabel.removeClass().addClass('label widget-label ' + self.config.icon); | ||||
|         this.thumbnailLabel.html(self.config.label); | ||||
|  | ||||
|         Object.keys(this.colorInputs).forEach(function (inputKey) { | ||||
|             var input = self.colorInputs[inputKey]; | ||||
|             input.on('change', function (value) { | ||||
|                 onColorInput(value, inputKey); | ||||
|             }); | ||||
|             input.set(self.config.style[inputKey]); | ||||
|             $('.t-style-input', self.domElement).append(input.getDOM()); | ||||
|         }); | ||||
|  | ||||
|         Object.keys(this.textInputs).forEach(function (inputKey) { | ||||
|             self.textInputs[inputKey].prop('value', self.config[inputKey] || ''); | ||||
|             self.textInputs[inputKey].on('input', function () { | ||||
|                 onTextInput(this, inputKey); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         this.deleteButton.on('click', this.remove); | ||||
|         this.duplicateButton.on('click', this.duplicate); | ||||
|         this.addConditionButton.on('click', function () { | ||||
|             self.initCondition(); | ||||
|         }); | ||||
|         this.toggleConfigButton.on('click', toggleConfig); | ||||
|         this.trigger.on('change', onTriggerInput); | ||||
|  | ||||
|         this.title.html(self.config.name); | ||||
|         this.description.html(self.config.description); | ||||
|         this.trigger.prop('value', self.config.trigger); | ||||
|  | ||||
|         this.grippy.on('mousedown', onDragStart); | ||||
|         this.widgetDnD.on('drop', function () { | ||||
|             this.domElement.show(); | ||||
|             $('.t-drag-indicator').hide(); | ||||
|         }, this); | ||||
|  | ||||
|         if (!this.conditionManager.loadCompleted()) { | ||||
|             this.config.expanded = false; | ||||
|         } | ||||
|  | ||||
|         if (!this.config.expanded) { | ||||
|             this.configArea.removeClass('expanded'); | ||||
|             this.toggleConfigButton.removeClass('expanded'); | ||||
|         } | ||||
|  | ||||
|         if (this.domainObject.configuration.ruleOrder.length === 2) { | ||||
|             $('.t-grippy', this.domElement).hide(); | ||||
|         } | ||||
|  | ||||
|         this.refreshConditions(); | ||||
|  | ||||
|         //if this is the default rule, hide elements that don't apply | ||||
|         if (this.config.id === 'default') { | ||||
|             $('.t-delete', this.domElement).hide(); | ||||
|             $('.t-widget-rule-config', this.domElement).hide(); | ||||
|             $('.t-grippy', this.domElement).hide(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return the DOM element representing this rule | ||||
|      * @return {Element} A DOM element | ||||
|      */ | ||||
|     Rule.prototype.getDOM = function () { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Unregister any event handlers registered with external sources | ||||
|      */ | ||||
|     Rule.prototype.destroy = function () { | ||||
|         Object.values(this.colorInputs).forEach(function (palette) { | ||||
|             palette.destroy(); | ||||
|         }); | ||||
|         this.iconInput.destroy(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this rule: supported callbacks are remove, change, | ||||
|      * conditionChange, and duplicate | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     Rule.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * An event handler for when a condition's configuration is modified | ||||
|      * @param {} value | ||||
|      * @param {string} property The path in the configuration to updateDomainObject | ||||
|      * @param {number} index The index of the condition that initiated this change | ||||
|      */ | ||||
|     Rule.prototype.onConditionChange = function (event) { | ||||
|         _.set(this.config.conditions[event.index], event.property, event.value); | ||||
|         this.generateDescription(); | ||||
|         this.updateDomainObject(); | ||||
|         this.eventEmitter.emit('conditionChange'); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * During a rule drag event, show the placeholder element after this rule | ||||
|      */ | ||||
|     Rule.prototype.showDragIndicator = function () { | ||||
|         $('.t-drag-indicator').hide(); | ||||
|         $('.t-drag-indicator', this.domElement).show(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Mutate thet domain object with this rule's local configuration | ||||
|      */ | ||||
|     Rule.prototype.updateDomainObject = function () { | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById.' + | ||||
|             this.config.id, this.config); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a property of this rule by key | ||||
|      * @param {string} prop They property key of this rule to get | ||||
|      * @return {} The queried property | ||||
|      */ | ||||
|     Rule.prototype.getProperty = function (prop) { | ||||
|         return this.config[prop]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Remove this rule from the domain object's configuration and invoke any | ||||
|      * registered remove callbacks | ||||
|      */ | ||||
|     Rule.prototype.remove = function () { | ||||
|         var ruleOrder = this.domainObject.configuration.ruleOrder, | ||||
|             ruleConfigById = this.domainObject.configuration.ruleConfigById, | ||||
|             self = this; | ||||
|  | ||||
|         ruleConfigById[self.config.id] = undefined; | ||||
|         _.remove(ruleOrder, function (ruleId) { | ||||
|             return ruleId === self.config.id; | ||||
|         }); | ||||
|  | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById); | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder); | ||||
|         this.destroy(); | ||||
|         this.eventEmitter.emit('remove'); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Makes a deep clone of this rule's configuration, and calls the duplicate event | ||||
|      * callback with the cloned configuration as an argument if one has been registered | ||||
|      */ | ||||
|     Rule.prototype.duplicate = function () { | ||||
|         var sourceRule = JSON.parse(JSON.stringify(this.config)); | ||||
|         sourceRule.expanded = true; | ||||
|         this.eventEmitter.emit('duplicate', sourceRule); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Initialze a new condition. If called with the sourceConfig and sourceIndex arguments, | ||||
|      * will insert a new condition with the provided configuration after the sourceIndex | ||||
|      * index. Otherwise, initializes a new blank rule and inserts it at the end | ||||
|      * of the list. | ||||
|      * @param {Object} [config] The configuration to initialize this rule from, | ||||
|      *                          consisting of sourceCondition and index fields | ||||
|      */ | ||||
|     Rule.prototype.initCondition = function (config) { | ||||
|         var ruleConfigById = this.domainObject.configuration.ruleConfigById, | ||||
|             newConfig, | ||||
|             sourceIndex = config && config.index, | ||||
|             defaultConfig = { | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             }; | ||||
|  | ||||
|         newConfig = (config !== undefined ? config.sourceCondition : defaultConfig); | ||||
|         if (sourceIndex !== undefined) { | ||||
|             ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig); | ||||
|         } else { | ||||
|             ruleConfigById[this.config.id].conditions.push(newConfig); | ||||
|         } | ||||
|         this.domainObject.configuration.ruleConfigById = ruleConfigById; | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshConditions(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Build {Condition} objects from configuration and rebuild associated view | ||||
|      */ | ||||
|     Rule.prototype.refreshConditions = function () { | ||||
|         var self = this, | ||||
|             $condition = null, | ||||
|             loopCnt = 0, | ||||
|             triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and '; | ||||
|  | ||||
|         self.conditions = []; | ||||
|         $('.t-condition', this.domElement).remove(); | ||||
|  | ||||
|         this.config.conditions.forEach(function (condition, index) { | ||||
|             var newCondition = new Condition(condition, index, self.conditionManager); | ||||
|             newCondition.on('remove', self.removeCondition, self); | ||||
|             newCondition.on('duplicate', self.initCondition, self); | ||||
|             newCondition.on('change', self.onConditionChange, self); | ||||
|             self.conditions.push(newCondition); | ||||
|         }); | ||||
|  | ||||
|         if (this.config.trigger === 'js') { | ||||
|             this.jsConditionArea.show(); | ||||
|             this.addConditionButton.hide(); | ||||
|         } else { | ||||
|             this.jsConditionArea.hide(); | ||||
|             this.addConditionButton.show(); | ||||
|             self.conditions.forEach(function (condition) { | ||||
|                 $condition = condition.getDOM(); | ||||
|                 $('li:last-of-type', self.conditionArea).before($condition); | ||||
|                 if (loopCnt > 0) { | ||||
|                     $('.t-condition-context', $condition).html(triggerContextStr + ' when'); | ||||
|                 } | ||||
|                 loopCnt++; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (self.conditions.length === 1) { | ||||
|             // Only one condition | ||||
|             self.conditions[0].hideButtons(); | ||||
|         } | ||||
|  | ||||
|         self.generateDescription(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Remove a condition from this rule's configuration at the given index | ||||
|      * @param {number} removeIndex The index of the condition to remove | ||||
|      */ | ||||
|     Rule.prototype.removeCondition = function (removeIndex) { | ||||
|         var ruleConfigById = this.domainObject.configuration.ruleConfigById, | ||||
|             conditions = ruleConfigById[this.config.id].conditions; | ||||
|  | ||||
|         _.remove(conditions, function (condition, index) { | ||||
|             return index === removeIndex; | ||||
|         }); | ||||
|  | ||||
|         this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshConditions(); | ||||
|         this.eventEmitter.emit('conditionChange'); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Build a human-readable description from this rule's conditions | ||||
|      */ | ||||
|     Rule.prototype.generateDescription = function () { | ||||
|         var description = '', | ||||
|             manager = this.conditionManager, | ||||
|             evaluator = manager.getEvaluator(), | ||||
|             name, | ||||
|             property, | ||||
|             operation, | ||||
|             self = this; | ||||
|  | ||||
|         if (this.config.conditions && this.config.id !== 'default') { | ||||
|             if (self.config.trigger === 'js') { | ||||
|                 description = 'when a custom JavaScript condition evaluates to true'; | ||||
|             } else { | ||||
|                 this.config.conditions.forEach(function (condition, index) { | ||||
|                     name = manager.getObjectName(condition.object); | ||||
|                     property = manager.getTelemetryPropertyName(condition.object, condition.key); | ||||
|                     operation = evaluator.getOperationDescription(condition.operation, condition.values); | ||||
|                     if (name || property || operation) { | ||||
|                         description += 'when ' + | ||||
|                             (name ? name + '\'s ' : '') + | ||||
|                             (property ? property + ' ' : '') + | ||||
|                             (operation ? operation + ' ' : '') + | ||||
|                             (self.config.trigger === 'any' ? ' OR ' : ' AND '); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (description.endsWith('OR ')) { | ||||
|             description = description.substring(0, description.length - 3); | ||||
|         } | ||||
|         if (description.endsWith('AND ')) { | ||||
|             description = description.substring(0, description.length - 4); | ||||
|         } | ||||
|         description = (description === '' ? this.config.description : description); | ||||
|         this.description.html(description); | ||||
|         this.config.description = description; | ||||
|         this.updateDomainObject(); | ||||
|     }; | ||||
|  | ||||
|     return Rule; | ||||
| }); | ||||
							
								
								
									
										387
									
								
								src/plugins/summaryWidget/src/SummaryWidget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								src/plugins/summaryWidget/src/SummaryWidget.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,387 @@ | ||||
| define([ | ||||
|     'text!../res/widgetTemplate.html', | ||||
|     './Rule', | ||||
|     './ConditionManager', | ||||
|     './TestDataManager', | ||||
|     './WidgetDnD', | ||||
|     'lodash', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     widgetTemplate, | ||||
|     Rule, | ||||
|     ConditionManager, | ||||
|     TestDataManager, | ||||
|     WidgetDnD, | ||||
|     _, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     //default css configuration for new rules | ||||
|     var DEFAULT_PROPS = { | ||||
|         'color': '#ffffff', | ||||
|         'background-color': '#38761d', | ||||
|         'border-color': 'rgba(0,0,0,0)' | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A Summary Widget object, which allows a user to configure rules based | ||||
|      * on telemetry producing domain objects, and update a compact display | ||||
|      * accordingly. | ||||
|      * @constructor | ||||
|      * @param {Object} domainObject The domain Object represented by this Widget | ||||
|      * @param {MCT} openmct An MCT instance | ||||
|      */ | ||||
|     function SummaryWidget(domainObject, openmct) { | ||||
|         this.domainObject = domainObject; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.domainObject.configuration = this.domainObject.configuration || {}; | ||||
|         this.domainObject.configuration.ruleConfigById = this.domainObject.configuration.ruleConfigById || {}; | ||||
|         this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || ['default']; | ||||
|         this.domainObject.configuration.testDataConfig = this.domainObject.configuration.testDataConfig || [{ | ||||
|             object: '', | ||||
|             key: '', | ||||
|             value: '' | ||||
|         }]; | ||||
|  | ||||
|         this.activeId = 'default'; | ||||
|         this.rulesById = {}; | ||||
|         this.domElement = $(widgetTemplate); | ||||
|         this.toggleRulesControl = $('.t-view-control-rules', this.domElement); | ||||
|         this.toggleTestDataControl = $('.t-view-control-test-data', this.domElement); | ||||
|         this.widgetButton = this.domElement.children('#widget'); | ||||
|         this.editing = false; | ||||
|         this.container = ''; | ||||
|         this.editListenerUnsubscribe = $.noop; | ||||
|  | ||||
|         this.outerWrapper = $('.widget-edit-holder', this.domElement); | ||||
|         this.ruleArea = $('#ruleArea', this.domElement); | ||||
|         this.configAreaRules = $('.widget-rules-wrapper', this.domElement); | ||||
|  | ||||
|         this.testDataArea = $('.widget-test-data', this.domElement); | ||||
|         this.addRuleButton = $('#addRule', this.domElement); | ||||
|  | ||||
|         this.conditionManager = new ConditionManager(this.domainObject, this.openmct); | ||||
|         this.testDataManager = new TestDataManager(this.domainObject, this.conditionManager, this.openmct); | ||||
|  | ||||
|         this.watchForChanges = this.watchForChanges.bind(this); | ||||
|         this.show = this.show.bind(this); | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|         this.addRule = this.addRule.bind(this); | ||||
|         this.onEdit = this.onEdit.bind(this); | ||||
|  | ||||
|         this.addHyperlink(domainObject.url, domainObject.openNewTab); | ||||
|         this.watchForChanges(openmct, domainObject); | ||||
|  | ||||
|         var id = this.domainObject.identifier.key, | ||||
|             self = this, | ||||
|             oldDomainObject, | ||||
|             statusCapability; | ||||
|  | ||||
|         /** | ||||
|          * Toggles the configuration area for test data in the view | ||||
|          * @private | ||||
|          */ | ||||
|         function toggleTestData() { | ||||
|             self.outerWrapper.toggleClass('expanded-widget-test-data'); | ||||
|             self.toggleTestDataControl.toggleClass('expanded'); | ||||
|         } | ||||
|         this.toggleTestDataControl.on('click', toggleTestData); | ||||
|  | ||||
|         /** | ||||
|          * Toggles the configuration area for rules in the view | ||||
|          * @private | ||||
|          */ | ||||
|         function toggleRules() { | ||||
|             self.outerWrapper.toggleClass('expanded-widget-rules'); | ||||
|             self.toggleRulesControl.toggleClass('expanded'); | ||||
|         } | ||||
|         this.toggleRulesControl.on('click', toggleRules); | ||||
|  | ||||
|         openmct.$injector.get('objectService') | ||||
|             .getObjects([id]) | ||||
|             .then(function (objs) { | ||||
|                 oldDomainObject = objs[id]; | ||||
|                 statusCapability = oldDomainObject.getCapability('status'); | ||||
|                 self.editListenerUnsubscribe = statusCapability.listen(self.onEdit); | ||||
|                 if (statusCapability.get('editing')) { | ||||
|                     self.onEdit(['editing']); | ||||
|                 } else { | ||||
|                     self.onEdit([]); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * adds or removes href to widget button and adds or removes openInNewTab | ||||
|      * @param {string} url String that denotes the url to be opened | ||||
|      * @param {string} openNewTab String that denotes wether to open link in new tab or not | ||||
|      */ | ||||
|     SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { | ||||
|         if (url) { | ||||
|             this.widgetButton.attr('href', url); | ||||
|         } else { | ||||
|             this.widgetButton.removeAttr('href'); | ||||
|         } | ||||
|  | ||||
|         if (openNewTab === 'newTab') { | ||||
|             this.widgetButton.attr('target', '_blank'); | ||||
|         } else { | ||||
|             this.widgetButton.removeAttr('target'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * adds a listener to the object to watch for any changes made by user | ||||
|      * only executes if changes are observed | ||||
|      * @param {openmct} Object Instance of OpenMCT | ||||
|      * @param {domainObject} Object instance of this object | ||||
|      */ | ||||
|     SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { | ||||
|         openmct.objects.observe(domainObject, '*', function (newDomainObject) { | ||||
|             if (newDomainObject.url !== this.domainObject.url || | ||||
|                     newDomainObject.openNewTab !== this.domainObject.openNewTab) { | ||||
|                 this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); | ||||
|             } | ||||
|         }.bind(this)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Builds the Summary Widget's DOM, performs other necessary setup, and attaches | ||||
|      * this Summary Widget's view to the supplied container. | ||||
|      * @param {element} container The DOM element that will contain this Summary | ||||
|      *                            Widget's view. | ||||
|      */ | ||||
|     SummaryWidget.prototype.show = function (container) { | ||||
|         var self = this; | ||||
|         this.container = container; | ||||
|         $(container).append(this.domElement); | ||||
|         $('.widget-test-data', this.domElement).append(this.testDataManager.getDOM()); | ||||
|         this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById); | ||||
|         this.initRule('default', 'Default'); | ||||
|         this.domainObject.configuration.ruleOrder.forEach(function (ruleId) { | ||||
|             self.initRule(ruleId); | ||||
|         }); | ||||
|         this.refreshRules(); | ||||
|         this.updateWidget(); | ||||
|         this.updateView(); | ||||
|  | ||||
|         this.addRuleButton.on('click', this.addRule); | ||||
|         this.conditionManager.on('receiveTelemetry', this.executeRules, this); | ||||
|         this.widgetDnD.on('drop', this.reorder, this); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, | ||||
|      * and clean up event handlers | ||||
|      */ | ||||
|     SummaryWidget.prototype.destroy = function (container) { | ||||
|         this.editListenerUnsubscribe(); | ||||
|         this.conditionManager.destroy(); | ||||
|         this.widgetDnD.destroy(); | ||||
|         Object.values(this.rulesById).forEach(function (rule) { | ||||
|             rule.destroy(); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A callback function for the Open MCT status capability listener. If the | ||||
|      * view representing the domain object is in edit mode, update the internal | ||||
|      * state and widget view accordingly. | ||||
|      * @param {string[]} status an array containing the domain object's current status | ||||
|      */ | ||||
|     SummaryWidget.prototype.onEdit = function (status) { | ||||
|         if (status && status.includes('editing')) { | ||||
|             this.editing = true; | ||||
|         } else { | ||||
|             this.editing = false; | ||||
|         } | ||||
|         this.updateView(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * If this view is currently in edit mode, show all rule configuration interfaces. | ||||
|      * Otherwise, hide them. | ||||
|      */ | ||||
|     SummaryWidget.prototype.updateView = function () { | ||||
|         if (this.editing) { | ||||
|             this.ruleArea.show(); | ||||
|             this.testDataArea.show(); | ||||
|             this.addRuleButton.show(); | ||||
|         } else { | ||||
|             this.ruleArea.hide(); | ||||
|             this.testDataArea.hide(); | ||||
|             this.addRuleButton.hide(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update the view from the current rule configuration and order | ||||
|      */ | ||||
|     SummaryWidget.prototype.refreshRules = function () { | ||||
|         var self = this, | ||||
|             ruleOrder = self.domainObject.configuration.ruleOrder, | ||||
|             rules = self.rulesById; | ||||
|  | ||||
|         self.ruleArea.html(''); | ||||
|         Object.values(ruleOrder).forEach(function (ruleId) { | ||||
|             self.ruleArea.append(rules[ruleId].getDOM()); | ||||
|         }); | ||||
|  | ||||
|         this.executeRules(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update the widget's appearance from the configuration of the active rule | ||||
|      */ | ||||
|     SummaryWidget.prototype.updateWidget = function () { | ||||
|         var activeRule = this.rulesById[this.activeId]; | ||||
|         this.applyStyle($('#widget', this.domElement), activeRule.getProperty('style')); | ||||
|         $('#widget', this.domElement).prop('title', activeRule.getProperty('message')); | ||||
|         $('#widgetLabel', this.domElement).html(activeRule.getProperty('label')); | ||||
|         $('#widgetLabel', this.domElement).removeClass().addClass('label widget-label ' + activeRule.getProperty('icon')); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the active rule and update the Widget's appearance. | ||||
|      */ | ||||
|     SummaryWidget.prototype.executeRules = function () { | ||||
|         this.activeId = this.conditionManager.executeRules( | ||||
|             this.domainObject.configuration.ruleOrder, | ||||
|             this.rulesById | ||||
|         ); | ||||
|         this.updateWidget(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Add a new rule to this widget | ||||
|      */ | ||||
|     SummaryWidget.prototype.addRule = function () { | ||||
|         var ruleCount = 0, | ||||
|             ruleId, | ||||
|             ruleOrder = this.domainObject.configuration.ruleOrder; | ||||
|  | ||||
|         while (Object.keys(this.rulesById).includes('rule' + ruleCount)) { | ||||
|             ruleCount = ++ruleCount; | ||||
|         } | ||||
|  | ||||
|         ruleId = 'rule' + ruleCount; | ||||
|         ruleOrder.push(ruleId); | ||||
|         this.domainObject.configuration.ruleOrder = ruleOrder; | ||||
|         this.updateDomainObject(); | ||||
|         this.initRule(ruleId, 'Rule'); | ||||
|         this.refreshRules(); | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Duplicate an existing widget rule from its configuration and splice it in | ||||
|      * after the rule it duplicates | ||||
|      * @param {Object} sourceConfig The configuration properties of the rule to be | ||||
|      *                              instantiated | ||||
|      */ | ||||
|     SummaryWidget.prototype.duplicateRule = function (sourceConfig) { | ||||
|         var ruleCount = 0, | ||||
|             ruleId, | ||||
|             sourceRuleId = sourceConfig.id, | ||||
|             ruleOrder = this.domainObject.configuration.ruleOrder, | ||||
|             ruleIds = Object.keys(this.rulesById); | ||||
|  | ||||
|         while (ruleIds.includes('rule' + ruleCount)) { | ||||
|             ruleCount = ++ruleCount; | ||||
|         } | ||||
|  | ||||
|         ruleId = 'rule' + ruleCount; | ||||
|         sourceConfig.id = ruleId; | ||||
|         sourceConfig.name += ' Copy'; | ||||
|         ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); | ||||
|         this.domainObject.configuration.ruleOrder = ruleOrder; | ||||
|         this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; | ||||
|         this.updateDomainObject(); | ||||
|         this.initRule(ruleId, sourceConfig.name); | ||||
|         this.refreshRules(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Initialze a new rule from a default configuration, or build a {Rule} object | ||||
|      * from it if already exists | ||||
|      * @param {string} ruleId An key to be used to identify this ruleId, or the key | ||||
|                               of the rule to be instantiated | ||||
|      * @param {string} ruleName The initial human-readable name of this rule | ||||
|      */ | ||||
|     SummaryWidget.prototype.initRule = function (ruleId, ruleName) { | ||||
|         var ruleConfig, | ||||
|             styleObj = {}; | ||||
|  | ||||
|         Object.assign(styleObj, DEFAULT_PROPS); | ||||
|         if (!this.domainObject.configuration.ruleConfigById[ruleId]) { | ||||
|             this.domainObject.configuration.ruleConfigById[ruleId] = { | ||||
|                 name: ruleName || 'Rule', | ||||
|                 label: this.domainObject.name, | ||||
|                 message: '', | ||||
|                 id: ruleId, | ||||
|                 icon: ' ', | ||||
|                 style: styleObj, | ||||
|                 description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule', | ||||
|                 conditions: [{ | ||||
|                     object: '', | ||||
|                     key: '', | ||||
|                     operation: '', | ||||
|                     values: [] | ||||
|                 }], | ||||
|                 jsCondition: '', | ||||
|                 trigger: 'any', | ||||
|                 expanded: 'true' | ||||
|             }; | ||||
|  | ||||
|         } | ||||
|         ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; | ||||
|         this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct, | ||||
|                                           this.conditionManager, this.widgetDnD, this.container); | ||||
|         this.rulesById[ruleId].on('remove', this.refreshRules, this); | ||||
|         this.rulesById[ruleId].on('duplicate', this.duplicateRule, this); | ||||
|         this.rulesById[ruleId].on('change', this.updateWidget, this); | ||||
|         this.rulesById[ruleId].on('conditionChange', this.executeRules, this); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Given two ruleIds, move the source rule after the target rule and update | ||||
|      * the view. | ||||
|      * @param {Object} event An event object representing this drop with draggingId | ||||
|      *                       and dropTarget fields | ||||
|      */ | ||||
|     SummaryWidget.prototype.reorder = function (event) { | ||||
|         var ruleOrder = this.domainObject.configuration.ruleOrder, | ||||
|             sourceIndex = ruleOrder.indexOf(event.draggingId), | ||||
|             targetIndex; | ||||
|  | ||||
|         if (event.draggingId !== event.dropTarget) { | ||||
|             ruleOrder.splice(sourceIndex, 1); | ||||
|             targetIndex = ruleOrder.indexOf(event.dropTarget); | ||||
|             ruleOrder.splice(targetIndex + 1, 0, event.draggingId); | ||||
|             this.domainObject.configuration.ruleOrder = ruleOrder; | ||||
|             this.updateDomainObject(); | ||||
|         } | ||||
|         this.refreshRules(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Apply a list of css properties to an element | ||||
|      * @param {element} elem The DOM element to which the rules will be applied | ||||
|      * @param {object} style an object representing the style | ||||
|      */ | ||||
|     SummaryWidget.prototype.applyStyle = function (elem, style) { | ||||
|         Object.keys(style).forEach(function (propId) { | ||||
|             elem.css(propId, style[propId]); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Mutate this domain object's configuration with the current local configuration | ||||
|      */ | ||||
|     SummaryWidget.prototype.updateDomainObject = function () { | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); | ||||
|     }; | ||||
|  | ||||
|     return SummaryWidget; | ||||
| }); | ||||
							
								
								
									
										177
									
								
								src/plugins/summaryWidget/src/TestDataItem.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/plugins/summaryWidget/src/TestDataItem.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| define([ | ||||
|     'text!../res/testDataItemTemplate.html', | ||||
|     './input/ObjectSelect', | ||||
|     './input/KeySelect', | ||||
|     'EventEmitter', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     itemTemplate, | ||||
|     ObjectSelect, | ||||
|     KeySelect, | ||||
|     EventEmitter, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * An object representing a single mock telemetry value | ||||
|      * @param {object} itemConfig the configuration for this item, consisting of | ||||
|      *                            object, key, and value fields | ||||
|      * @param {number} index the index of this TestDataItem object in the data | ||||
|      *                 model of its parent {TestDataManager} o be injected into callbacks | ||||
|      *                 for removes | ||||
|      * @param {ConditionManager} conditionManager a conditionManager instance | ||||
|      *                           for populating selects with configuration data | ||||
|      * @constructor | ||||
|      */ | ||||
|     function TestDataItem(itemConfig, index, conditionManager) { | ||||
|         this.config = itemConfig; | ||||
|         this.index = index; | ||||
|         this.conditionManager = conditionManager; | ||||
|  | ||||
|         this.domElement = $(itemTemplate); | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['remove', 'duplicate', 'change']; | ||||
|  | ||||
|         this.deleteButton = $('.t-delete', this.domElement); | ||||
|         this.duplicateButton = $('.t-duplicate', this.domElement); | ||||
|  | ||||
|         this.selects = {}; | ||||
|         this.valueInputs = []; | ||||
|  | ||||
|         this.remove = this.remove.bind(this); | ||||
|         this.duplicate = this.duplicate.bind(this); | ||||
|  | ||||
|         var self = this; | ||||
|  | ||||
|         /** | ||||
|          * A change event handler for this item's select inputs, which also invokes | ||||
|          * change callbacks registered with this item | ||||
|          * @param {string} value The new value of this select item | ||||
|          * @param {string} property The property of this item to modify | ||||
|          * @private | ||||
|          */ | ||||
|         function onSelectChange(value, property) { | ||||
|             if (property === 'key') { | ||||
|                 self.generateValueInput(value); | ||||
|             } | ||||
|             self.eventEmitter.emit('change', { | ||||
|                 value: value, | ||||
|                 property: property, | ||||
|                 index: self.index | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * An input event handler for this item's value field. Invokes any change | ||||
|          * callbacks associated with this item | ||||
|          * @param {Event} event The input event that initiated this callback | ||||
|          * @private | ||||
|          */ | ||||
|         function onValueInput(event) { | ||||
|             var elem = event.target, | ||||
|                 value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber); | ||||
|  | ||||
|             self.eventEmitter.emit('change', { | ||||
|                 value: value, | ||||
|                 property: 'value', | ||||
|                 index: self.index | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.deleteButton.on('click', this.remove); | ||||
|         this.duplicateButton.on('click', this.duplicate); | ||||
|  | ||||
|         this.selects.object = new ObjectSelect(this.config, this.conditionManager); | ||||
|         this.selects.key = new KeySelect( | ||||
|             this.config, | ||||
|             this.selects.object, | ||||
|             this.conditionManager, | ||||
|             function (value) { | ||||
|                 onSelectChange(value, 'key'); | ||||
|             }); | ||||
|  | ||||
|         this.selects.object.on('change', function (value) { | ||||
|             onSelectChange(value, 'object'); | ||||
|         }); | ||||
|  | ||||
|         Object.values(this.selects).forEach(function (select) { | ||||
|             $('.t-configuration', self.domElement).append(select.getDOM()); | ||||
|         }); | ||||
|  | ||||
|         $(this.domElement).on('input', 'input', onValueInput); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the DOM associated with this element's view | ||||
|      * @return {Element} | ||||
|      */ | ||||
|     TestDataItem.prototype.getDOM = function (container) { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this item: supported callbacks are remove, change, | ||||
|      * and duplicate | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     TestDataItem.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Hide the appropriate inputs when this is the only item | ||||
|      */ | ||||
|     TestDataItem.prototype.hideButtons = function () { | ||||
|         this.deleteButton.hide(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Remove this item from the configuration. Invokes any registered | ||||
|      * remove callbacks | ||||
|      */ | ||||
|     TestDataItem.prototype.remove = function () { | ||||
|         var self = this; | ||||
|         this.eventEmitter.emit('remove', self.index); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Makes a deep clone of this item's configuration, and invokes any registered | ||||
|      * duplicate callbacks with the cloned configuration as an argument | ||||
|      */ | ||||
|     TestDataItem.prototype.duplicate = function () { | ||||
|         var sourceItem = JSON.parse(JSON.stringify(this.config)), | ||||
|             self = this; | ||||
|         this.eventEmitter.emit('duplicate', { | ||||
|             sourceItem: sourceItem, | ||||
|             index: self.index | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * When a telemetry property key is selected, create the appropriate value input | ||||
|      * and add it to the view | ||||
|      * @param {string} key The key of currently selected telemetry property | ||||
|      */ | ||||
|     TestDataItem.prototype.generateValueInput = function (key) { | ||||
|         var evaluator = this.conditionManager.getEvaluator(), | ||||
|             inputArea = $('.t-value-inputs', this.domElement), | ||||
|             dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key), | ||||
|             inputType = evaluator.getInputTypeById(dataType); | ||||
|  | ||||
|         inputArea.html(''); | ||||
|         if (inputType) { | ||||
|             if (!this.config.value) { | ||||
|                 this.config.value = (inputType === 'number' ? 0 : ''); | ||||
|             } | ||||
|             this.valueInput = $('<input class="sm" type = "' + inputType + '" value = "' + this.config.value + '"> </input>').get(0); | ||||
|             inputArea.append(this.valueInput); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return TestDataItem; | ||||
| }); | ||||
							
								
								
									
										190
									
								
								src/plugins/summaryWidget/src/TestDataManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/plugins/summaryWidget/src/TestDataManager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| define([ | ||||
|     'text!../res/testDataTemplate.html', | ||||
|     './TestDataItem', | ||||
|     'zepto', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     testDataTemplate, | ||||
|     TestDataItem, | ||||
|     $, | ||||
|     _ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Controls the input and usage of test data in the summary widget. | ||||
|      * @constructor | ||||
|      * @param {Object} domainObject The summary widget domain object | ||||
|      * @param {ConditionManager} conditionManager A conditionManager instance | ||||
|      * @param {MCT} openmct and MCT instance | ||||
|      */ | ||||
|     function TestDataManager(domainObject, conditionManager, openmct) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.domainObject = domainObject; | ||||
|         this.manager = conditionManager; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.evaluator = this.manager.getEvaluator(); | ||||
|         this.domElement = $(testDataTemplate); | ||||
|         this.config = this.domainObject.configuration.testDataConfig; | ||||
|         this.testCache = {}; | ||||
|  | ||||
|         this.itemArea = $('.t-test-data-config', this.domElement); | ||||
|         this.addItemButton = $('.add-test-condition', this.domElement); | ||||
|         this.testDataInput = $('.t-test-data-checkbox', this.domElement); | ||||
|  | ||||
|         /** | ||||
|          * Toggles whether the associated {ConditionEvaluator} uses the actual | ||||
|          * subscription cache or the test data cache | ||||
|          * @param {Event} event The change event that triggered this callback | ||||
|          * @private | ||||
|          */ | ||||
|         function toggleTestData(event) { | ||||
|             var elem = event.target; | ||||
|             self.evaluator.useTestData(elem.checked); | ||||
|             self.updateTestCache(); | ||||
|         } | ||||
|  | ||||
|         this.addItemButton.on('click', function () { | ||||
|             self.initItem(); | ||||
|         }); | ||||
|         this.testDataInput.on('change', toggleTestData); | ||||
|  | ||||
|         this.evaluator.setTestDataCache(this.testCache); | ||||
|         this.evaluator.useTestData(false); | ||||
|  | ||||
|         this.refreshItems(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the DOM element representing this test data manager in the view | ||||
|      */ | ||||
|     TestDataManager.prototype.getDOM = function () { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Initialze a new test data item, either from a source configuration, or with | ||||
|      * the default empty configuration | ||||
|      * @param {Object} [config] An object with sourceItem and index fields to instantiate | ||||
|      *                          this rule from, optional | ||||
|      */ | ||||
|     TestDataManager.prototype.initItem = function (config) { | ||||
|         var sourceIndex = config && config.index, | ||||
|         defaultItem = { | ||||
|             object: '', | ||||
|             key: '', | ||||
|             value: '' | ||||
|         }, | ||||
|         newItem; | ||||
|  | ||||
|         newItem = (config !== undefined ? config.sourceItem : defaultItem); | ||||
|         if (sourceIndex !== undefined) { | ||||
|             this.config.splice(sourceIndex + 1, 0, newItem); | ||||
|         } else { | ||||
|             this.config.push(newItem); | ||||
|         } | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshItems(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Remove an item from this TestDataManager at the given index | ||||
|      * @param {number} removeIndex The index of the item to remove | ||||
|      */ | ||||
|     TestDataManager.prototype.removeItem = function (removeIndex) { | ||||
|         _.remove(this.config, function (item, index) { | ||||
|             return index === removeIndex; | ||||
|         }); | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshItems(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Change event handler for the test data items which compose this | ||||
|      * test data generateor | ||||
|      * @param {Object} event An object representing this event, with value, property, | ||||
|      *                       and index fields | ||||
|      */ | ||||
|     TestDataManager.prototype.onItemChange = function (event) { | ||||
|         this.config[event.index][event.property] = event.value; | ||||
|         this.updateDomainObject(); | ||||
|         this.updateTestCache(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Builds the test cache from the current item configuration, and passes | ||||
|      * the new test cache to the associated {ConditionEvaluator} instance | ||||
|      */ | ||||
|     TestDataManager.prototype.updateTestCache = function () { | ||||
|         this.generateTestCache(); | ||||
|         this.evaluator.setTestDataCache(this.testCache); | ||||
|         this.manager.triggerTelemetryCallback(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Intantiate {TestDataItem} objects from the current configuration, and | ||||
|      * update the view accordingly | ||||
|      */ | ||||
|     TestDataManager.prototype.refreshItems = function () { | ||||
|         var self = this; | ||||
|  | ||||
|         self.items = []; | ||||
|         $('.t-test-data-item', this.domElement).remove(); | ||||
|  | ||||
|         this.config.forEach(function (item, index) { | ||||
|             var newItem = new TestDataItem(item, index, self.manager); | ||||
|             newItem.on('remove', self.removeItem, self); | ||||
|             newItem.on('duplicate', self.initItem, self); | ||||
|             newItem.on('change', self.onItemChange, self); | ||||
|             self.items.push(newItem); | ||||
|         }); | ||||
|  | ||||
|         self.items.forEach(function (item) { | ||||
|             // $('li:last-of-type', self.itemArea).before(item.getDOM()); | ||||
|             self.itemArea.prepend(item.getDOM()); | ||||
|         }); | ||||
|  | ||||
|         if (self.items.length === 1) { | ||||
|             self.items[0].hideButtons(); | ||||
|         } | ||||
|  | ||||
|         this.updateTestCache(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Builds a test data cache in the format of a telemetry subscription cache | ||||
|      * as expected by a {ConditionEvaluator} | ||||
|      */ | ||||
|     TestDataManager.prototype.generateTestCache = function () { | ||||
|         var testCache = this.testCache, | ||||
|             manager = this.manager, | ||||
|             compositionObjs = manager.getComposition(), | ||||
|             metadata; | ||||
|  | ||||
|         testCache = {}; | ||||
|         Object.keys(compositionObjs).forEach(function (id) { | ||||
|             testCache[id] = {}; | ||||
|             metadata = manager.getTelemetryMetadata(id); | ||||
|             Object.keys(metadata).forEach(function (key) { | ||||
|                 testCache[id][key] = ''; | ||||
|             }); | ||||
|         }); | ||||
|         this.config.forEach(function (item) { | ||||
|             if (testCache[item.object]) { | ||||
|                 testCache[item.object][item.key] = item.value; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.testCache = testCache; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update the domain object configuration associated with this test data manager | ||||
|      */ | ||||
|     TestDataManager.prototype.updateDomainObject = function () { | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); | ||||
|     }; | ||||
|  | ||||
|     return TestDataManager; | ||||
| }); | ||||
							
								
								
									
										167
									
								
								src/plugins/summaryWidget/src/WidgetDnD.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/plugins/summaryWidget/src/WidgetDnD.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| define([ | ||||
|     'text!../res/ruleImageTemplate.html', | ||||
|     'EventEmitter', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     ruleImageTemplate, | ||||
|     EventEmitter, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Manages the Sortable List interface for reordering rules by drag and drop | ||||
|      * @param {Element} container The DOM element that contains this Summary Widget's view | ||||
|      * @param {string[]} ruleOrder An array of rule IDs representing the current rule order | ||||
|      * @param {Object} rulesById An object mapping rule IDs to rule configurations | ||||
|      */ | ||||
|     function WidgetDnD(container, ruleOrder, rulesById) { | ||||
|         this.container = container; | ||||
|         this.ruleOrder = ruleOrder; | ||||
|         this.rulesById = rulesById; | ||||
|  | ||||
|         this.imageContainer = $(ruleImageTemplate); | ||||
|         this.image = $('.t-drag-rule-image', this.imageContainer); | ||||
|         this.draggingId = ''; | ||||
|         this.draggingRulePrevious = ''; | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['drop']; | ||||
|  | ||||
|         this.drag = this.drag.bind(this); | ||||
|         this.drop = this.drop.bind(this); | ||||
|  | ||||
|         $(this.container).on('mousemove', this.drag); | ||||
|         $(document).on('mouseup', this.drop); | ||||
|         $(this.container).before(this.imageContainer); | ||||
|         $(this.imageContainer).hide(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove event listeners registered to elements external to the widget | ||||
|      */ | ||||
|     WidgetDnD.prototype.destroy = function () { | ||||
|         $(this.container).off('mousemove', this.drag); | ||||
|         $(document).off('mouseup', this.drop); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this WidgetDnD: supported callback is drop | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     WidgetDnD.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Sets the image for the dragged element to the given DOM element | ||||
|      * @param {Element} image The HTML element to set as the drap image | ||||
|      */ | ||||
|     WidgetDnD.prototype.setDragImage = function (image) { | ||||
|         this.image.html(image); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Calculate where this rule has been dragged relative to the other rules | ||||
|      * @param {Event} event The mousemove or mouseup event that triggered this | ||||
|                             event handler | ||||
|      * @return {string} The ID of the rule whose drag indicator should be displayed | ||||
|      */ | ||||
|     WidgetDnD.prototype.getDropLocation = function (event) { | ||||
|         var ruleOrder = this.ruleOrder, | ||||
|             rulesById = this.rulesById, | ||||
|             draggingId = this.draggingId, | ||||
|             offset, | ||||
|             y, | ||||
|             height, | ||||
|             dropY = event.pageY, | ||||
|             target = ''; | ||||
|  | ||||
|         ruleOrder.forEach(function (ruleId, index) { | ||||
|             offset = rulesById[ruleId].getDOM().offset(); | ||||
|             y = offset.top; | ||||
|             height = offset.height; | ||||
|             if (index === 0) { | ||||
|                 if (dropY < y + 7 * height / 3) { | ||||
|                     target = ruleId; | ||||
|                 } | ||||
|             } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) { | ||||
|                 if (y + height / 3 < dropY) { | ||||
|                     target = ruleId; | ||||
|                 } | ||||
|             } else { | ||||
|                 if (y + height / 3 < dropY && dropY < y + 7 * height / 3) { | ||||
|                     target = ruleId; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         return target; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Called by a {Rule} instance that initiates a drag gesture | ||||
|      * @param {string} ruleId The identifier of the rule which is being dragged | ||||
|      */ | ||||
|     WidgetDnD.prototype.dragStart = function (ruleId) { | ||||
|         var ruleOrder = this.ruleOrder; | ||||
|         this.draggingId = ruleId; | ||||
|         this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; | ||||
|         this.rulesById[this.draggingRulePrevious].showDragIndicator(); | ||||
|         this.imageContainer.show(); | ||||
|         this.imageContainer.offset({ | ||||
|             top: event.pageY - this.image.height() / 2, | ||||
|             left: event.pageX - $('.t-grippy', this.image).width() | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * An event handler for a mousemove event, once a rule has begun a drag gesture | ||||
|      * @param {Event} event The mousemove event that triggered this callback | ||||
|      */ | ||||
|     WidgetDnD.prototype.drag = function (event) { | ||||
|         var dragTarget; | ||||
|         if (this.draggingId && this.draggingId !== '') { | ||||
|             event.preventDefault(); | ||||
|             dragTarget = this.getDropLocation(event); | ||||
|             this.imageContainer.offset({ | ||||
|                 top: event.pageY - this.image.height() / 2, | ||||
|                 left: event.pageX - $('.t-grippy', this.image).width() | ||||
|             }); | ||||
|             if (this.rulesById[dragTarget]) { | ||||
|                 this.rulesById[dragTarget].showDragIndicator(); | ||||
|             } else { | ||||
|                 this.rulesById[this.draggingRulePrevious].showDragIndicator(); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Handles the mouseup event that corresponds to the user dropping the rule | ||||
|      * in its final location. Invokes any registered drop callbacks with the dragged | ||||
|      * rule's ID and the ID of the target rule that the dragged rule should be | ||||
|      * inserted after | ||||
|      * @param {Event} event The mouseup event that triggered this callback | ||||
|      */ | ||||
|     WidgetDnD.prototype.drop = function (event) { | ||||
|         var dropTarget = this.getDropLocation(event), | ||||
|             draggingId = this.draggingId; | ||||
|  | ||||
|         if (this.draggingId && this.draggingId !== '') { | ||||
|             if (!this.rulesById[dropTarget]) { | ||||
|                 dropTarget = this.draggingId; | ||||
|             } | ||||
|             this.eventEmitter.emit('drop', { | ||||
|                 draggingId: draggingId, | ||||
|                 dropTarget: dropTarget | ||||
|             }); | ||||
|             this.draggingId = ''; | ||||
|             this.draggingRulePrevious = ''; | ||||
|             this.imageContainer.hide(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return WidgetDnD; | ||||
| }); | ||||
							
								
								
									
										64
									
								
								src/plugins/summaryWidget/src/input/ColorPalette.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/summaryWidget/src/input/ColorPalette.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| define([ | ||||
|     './Palette', | ||||
|     'zepto' | ||||
| ], | ||||
| function ( | ||||
|     Palette, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     //The colors that will be used to instantiate this palette if none are provided | ||||
|     var DEFAULT_COLORS = [ | ||||
|         '#000000','#434343','#666666','#999999','#b7b7b7','#cccccc','#d9d9d9','#efefef','#f3f3f3','#ffffff', | ||||
|         '#980000','#ff0000','#ff9900','#ffff00','#00ff00','#00ffff','#4a86e8','#0000ff','#9900ff','#ff00ff', | ||||
|         '#e6b8af','#f4cccc','#fce5cd','#fff2cc','#d9ead3','#d0e0e3','#c9daf8','#cfe2f3','#d9d2e9','#ead1dc', | ||||
|         '#dd7e6b','#dd7e6b','#f9cb9c','#ffe599','#b6d7a8','#a2c4c9','#a4c2f4','#9fc5e8','#b4a7d6','#d5a6bd', | ||||
|         '#cc4125','#e06666','#f6b26b','#ffd966','#93c47d','#76a5af','#6d9eeb','#6fa8dc','#8e7cc3','#c27ba0', | ||||
|         '#a61c00','#cc0000','#e69138','#f1c232','#6aa84f','#45818e','#3c78d8','#3d85c6','#674ea7','#a64d79', | ||||
|         '#85200c','#990000','#b45f06','#bf9000','#38761d','#134f5c','#1155cc','#0b5394','#351c75','#741b47', | ||||
|         '#5b0f00','#660000','#783f04','#7f6000','#274e13','#0c343d','#1c4587','#073763','#20124d','#4c1130' | ||||
|       ]; | ||||
|  | ||||
|     /** | ||||
|      * Instantiates a new Open MCT Color Palette input | ||||
|      * @constructor | ||||
|      * @param {string} cssClass The class name of the icon which should be applied | ||||
|      *                          to this palette | ||||
|      * @param {Element} container The view that contains this palette | ||||
|      * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette | ||||
|      */ | ||||
|     function ColorPalette(cssClass, container, colors) { | ||||
|         this.colors = colors || DEFAULT_COLORS; | ||||
|         this.palette = new Palette(cssClass, container, this.colors); | ||||
|  | ||||
|         this.palette.setNullOption('rgba(0,0,0,0)'); | ||||
|  | ||||
|         var domElement = $(this.palette.getDOM()), | ||||
|             self = this; | ||||
|  | ||||
|         $('.s-menu-button', domElement).addClass('t-color-palette-menu-button'); | ||||
|         $('.t-swatch', domElement).addClass('color-swatch'); | ||||
|         $('.l-palette', domElement).addClass('l-color-palette'); | ||||
|  | ||||
|         $('.s-palette-item', domElement).each(function () { | ||||
|             var elem = this; | ||||
|             $(elem).css('background-color', elem.dataset.item); | ||||
|         }); | ||||
|  | ||||
|         /** | ||||
|          * Update this palette's current selection indicator with the style | ||||
|          * of the currently selected item | ||||
|          * @private | ||||
|          */ | ||||
|         function updateSwatch() { | ||||
|             var color = self.palette.getCurrent(); | ||||
|             $('.color-swatch', domElement).css('background-color', color); | ||||
|         } | ||||
|  | ||||
|         this.palette.on('change', updateSwatch); | ||||
|  | ||||
|         return this.palette; | ||||
|     } | ||||
|  | ||||
|     return ColorPalette; | ||||
| }); | ||||
							
								
								
									
										80
									
								
								src/plugins/summaryWidget/src/input/IconPalette.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/plugins/summaryWidget/src/input/IconPalette.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| define([ | ||||
|       './Palette', | ||||
|       'zepto' | ||||
| ], function ( | ||||
|     Palette, | ||||
|     $ | ||||
| ) { | ||||
|     //The icons that will be used to instantiate this palette if none are provided | ||||
|     var DEFAULT_ICONS = [ | ||||
|         'icon-alert-rect', | ||||
|         'icon-alert-triangle', | ||||
|         'icon-arrow-down', | ||||
|         'icon-arrow-left', | ||||
|         'icon-arrow-right', | ||||
|         'icon-arrow-double-up', | ||||
|         'icon-arrow-tall-up', | ||||
|         'icon-arrow-tall-down', | ||||
|         'icon-arrow-double-down', | ||||
|         'icon-arrow-up', | ||||
|         'icon-asterisk', | ||||
|         'icon-bell', | ||||
|         'icon-check', | ||||
|         'icon-eye-open', | ||||
|         'icon-gear', | ||||
|         'icon-hourglass', | ||||
|         'icon-info', | ||||
|         'icon-link', | ||||
|         'icon-lock', | ||||
|         'icon-people', | ||||
|         'icon-person', | ||||
|         'icon-plus', | ||||
|         'icon-trash', | ||||
|         'icon-x' | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
|      * Instantiates a new Open MCT Icon Palette input | ||||
|      * @constructor | ||||
|      * @param {string} cssClass The class name of the icon which should be applied | ||||
|      *                          to this palette | ||||
|      * @param {Element} container The view that contains this palette | ||||
|      * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette | ||||
|      */ | ||||
|     function IconPalette(cssClass, container, icons) { | ||||
|         this.icons = icons || DEFAULT_ICONS; | ||||
|         this.palette = new Palette(cssClass, container, this.icons); | ||||
|  | ||||
|         this.palette.setNullOption(' '); | ||||
|         this.oldIcon = this.palette.current || ' '; | ||||
|  | ||||
|         var domElement = $(this.palette.getDOM()), | ||||
|             self = this; | ||||
|  | ||||
|         $('.s-menu-button', domElement).addClass('t-icon-palette-menu-button'); | ||||
|         $('.t-swatch', domElement).addClass('icon-swatch'); | ||||
|         $('.l-palette', domElement).addClass('l-icon-palette'); | ||||
|  | ||||
|         $('.s-palette-item', domElement).each(function () { | ||||
|             var elem = this; | ||||
|             $(elem).addClass(elem.dataset.item); | ||||
|         }); | ||||
|  | ||||
|         /** | ||||
|          * Update this palette's current selection indicator with the style | ||||
|          * of the currently selected item | ||||
|          * @private | ||||
|          */ | ||||
|         function updateSwatch() { | ||||
|             $('.icon-swatch', domElement).removeClass(self.oldIcon) | ||||
|                 .addClass(self.palette.getCurrent()); | ||||
|             self.oldIcon = self.palette.getCurrent(); | ||||
|         } | ||||
|  | ||||
|         this.palette.on('change', updateSwatch); | ||||
|  | ||||
|         return this.palette; | ||||
|     } | ||||
|  | ||||
|     return IconPalette; | ||||
| }); | ||||
							
								
								
									
										90
									
								
								src/plugins/summaryWidget/src/input/KeySelect.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/plugins/summaryWidget/src/input/KeySelect.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| define(['./Select'], function (Select) { | ||||
|  | ||||
|     /** | ||||
|      * Create a {Select} element whose composition is dynamically updated with | ||||
|      * the telemetry fields of a particular domain object | ||||
|      * @constructor | ||||
|      * @param {Object} config The current state of this select. Must have object | ||||
|      *                        and key fields | ||||
|      * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which | ||||
|      *                                    this KeySelect should listen to for change | ||||
|      *                                    events | ||||
|      * @param {ConditionManager} manager A ConditionManager instance from which | ||||
|      *                                   to receive telemetry metadata | ||||
|      * @param {function} changeCallback A change event callback to register with this | ||||
|      *                                  select on initialization | ||||
|      */ | ||||
|     var NULLVALUE = '- Select Field -'; | ||||
|  | ||||
|     function KeySelect(config, objectSelect, manager, changeCallback) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.config = config; | ||||
|         this.objectSelect = objectSelect; | ||||
|         this.manager = manager; | ||||
|  | ||||
|         this.select = new Select(); | ||||
|         this.select.hide(); | ||||
|         this.select.addOption('', NULLVALUE); | ||||
|         if (changeCallback) { | ||||
|             this.select.on('change', changeCallback); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Change event handler for the {ObjectSelect} to which this KeySelect instance | ||||
|          * is linked. Loads the new object's metadata and updates its select element's | ||||
|          * composition. | ||||
|          * @param {Object} key The key identifying the newly selected domain object | ||||
|          * @private | ||||
|          */ | ||||
|         function onObjectChange(key) { | ||||
|             var selected = self.manager.metadataLoadCompleted() ? self.select.getSelected() : self.config.key; | ||||
|             self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {}; | ||||
|             self.generateOptions(); | ||||
|             self.select.setSelected(selected); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Event handler for the intial metadata load event from the associated | ||||
|          * ConditionManager. Retreives metadata from the manager and populates | ||||
|          * the select element. | ||||
|          * @private | ||||
|          */ | ||||
|         function onMetadataLoad() { | ||||
|             if (self.manager.getTelemetryMetadata(self.config.object)) { | ||||
|                 self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object); | ||||
|                 self.generateOptions(); | ||||
|             } | ||||
|             self.select.setSelected(self.config.key); | ||||
|         } | ||||
|  | ||||
|         if (self.manager.metadataLoadCompleted()) { | ||||
|             onMetadataLoad(); | ||||
|         } | ||||
|  | ||||
|         this.objectSelect.on('change', onObjectChange); | ||||
|         this.manager.on('metadata', onMetadataLoad); | ||||
|  | ||||
|         return this.select; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populate this select with options based on its current composition | ||||
|      */ | ||||
|     KeySelect.prototype.generateOptions = function () { | ||||
|         var items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { | ||||
|             return [metaDatum[0], metaDatum[1].name]; | ||||
|         }); | ||||
|         items.splice(0, 0, ['',NULLVALUE]); | ||||
|         this.select.setOptions(items); | ||||
|  | ||||
|         if (this.select.options.length < 2) { | ||||
|             this.select.hide(); | ||||
|         } else if (this.select.options.length > 1) { | ||||
|             this.select.show(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return KeySelect; | ||||
|  | ||||
| }); | ||||
							
								
								
									
										87
									
								
								src/plugins/summaryWidget/src/input/ObjectSelect.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/plugins/summaryWidget/src/input/ObjectSelect.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| define(['./Select'], function (Select) { | ||||
|  | ||||
|     /** | ||||
|      * Create a {Select} element whose composition is dynamically updated with | ||||
|      * the current composition of the Summary Widget | ||||
|      * @constructor | ||||
|      * @param {Object} config The current state of this select. Must have an | ||||
|      *                        object field | ||||
|      * @param {ConditionManager} manager A ConditionManager instance from which | ||||
|      *                                   to receive the current composition status | ||||
|      * @param {string[][]} baseOptions A set of [value, label] keyword pairs to | ||||
|      *                                 display regardless of the composition state | ||||
|      */ | ||||
|     function ObjectSelect(config, manager, baseOptions) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.config = config; | ||||
|         this.manager = manager; | ||||
|  | ||||
|         this.select = new Select(); | ||||
|         this.baseOptions = [['', '- Select Telemetry -']]; | ||||
|         if (baseOptions) { | ||||
|             this.baseOptions = this.baseOptions.concat(baseOptions); | ||||
|         } | ||||
|  | ||||
|         this.baseOptions.forEach(function (option) { | ||||
|             self.select.addOption(option[0], option[1]); | ||||
|         }); | ||||
|  | ||||
|         this.compositionObjs = this.manager.getComposition(); | ||||
|         self.generateOptions(); | ||||
|  | ||||
|         /** | ||||
|          * Add a new composition object to this select when a composition added | ||||
|          * is detected on the Summary Widget | ||||
|          * @param {Object} obj The newly added domain object | ||||
|          * @private | ||||
|          */ | ||||
|         function onCompositionAdd(obj) { | ||||
|             self.select.addOption(obj.identifier.key, obj.name); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Refresh the composition of this select when a domain object is removed | ||||
|          * from the Summary Widget's composition | ||||
|          * @private | ||||
|          */ | ||||
|         function onCompositionRemove() { | ||||
|             var selected = self.select.getSelected(); | ||||
|             self.generateOptions(); | ||||
|             self.select.setSelected(selected); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Defer setting the selected state on initial load until load is complete | ||||
|          * @private | ||||
|          */ | ||||
|         function onCompositionLoad() { | ||||
|             self.select.setSelected(self.config.object); | ||||
|         } | ||||
|  | ||||
|         this.manager.on('add', onCompositionAdd); | ||||
|         this.manager.on('remove', onCompositionRemove); | ||||
|         this.manager.on('load', onCompositionLoad); | ||||
|  | ||||
|         if (this.manager.loadCompleted()) { | ||||
|             onCompositionLoad(); | ||||
|         } | ||||
|  | ||||
|         return this.select; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populate this select with options based on its current composition | ||||
|      */ | ||||
|     ObjectSelect.prototype.generateOptions = function () { | ||||
|         var items = Object.values(this.compositionObjs).map(function (obj) { | ||||
|             return [obj.identifier.key, obj.name]; | ||||
|         }); | ||||
|         this.baseOptions.forEach(function (option, index) { | ||||
|             items.splice(index, 0, option); | ||||
|         }); | ||||
|         this.select.setOptions(items); | ||||
|     }; | ||||
|  | ||||
|     return ObjectSelect; | ||||
| }); | ||||
							
								
								
									
										114
									
								
								src/plugins/summaryWidget/src/input/OperationSelect.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/plugins/summaryWidget/src/input/OperationSelect.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| define(['./Select'], function (Select) { | ||||
|  | ||||
|     /** | ||||
|      * Create a {Select} element whose composition is dynamically updated with | ||||
|      * the operations applying to a particular telemetry property | ||||
|      * @constructor | ||||
|      * @param {Object} config The current state of this select. Must have object, | ||||
|      *                        key, and operation fields | ||||
|      * @param {KeySelect} keySelect The linked Key Select instance to which | ||||
|      *                              this OperationSelect should listen to for change | ||||
|      *                              events | ||||
|      * @param {ConditionManager} manager A ConditionManager instance from which | ||||
|      *                                   to receive telemetry metadata | ||||
|      * @param {function} changeCallback A change event callback to register with this | ||||
|      *                                  select on initialization | ||||
|      */ | ||||
|     var NULLVALUE = '- Select Comparison -'; | ||||
|  | ||||
|     function OperationSelect(config, keySelect, manager, changeCallback) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.config = config; | ||||
|         this.keySelect = keySelect; | ||||
|         this.manager = manager; | ||||
|  | ||||
|         this.operationKeys = []; | ||||
|         this.evaluator = this.manager.getEvaluator(); | ||||
|         this.loadComplete = false; | ||||
|  | ||||
|         this.select = new Select(); | ||||
|         this.select.hide(); | ||||
|         this.select.addOption('', NULLVALUE); | ||||
|         if (changeCallback) { | ||||
|             this.select.on('change', changeCallback); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Change event handler for the {KeySelect} to which this OperationSelect instance | ||||
|          * is linked. Loads the operations applicable to the given telemetry property and updates | ||||
|          * its select element's composition | ||||
|          * @param {Object} key The key identifying the newly selected property | ||||
|          * @private | ||||
|          */ | ||||
|         function onKeyChange(key) { | ||||
|             var selected = self.config.operation; | ||||
|             if (self.manager.metadataLoadCompleted()) { | ||||
|                 self.loadOptions(key); | ||||
|                 self.generateOptions(); | ||||
|                 self.select.setSelected(selected); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Event handler for the intial metadata load event from the associated | ||||
|          * ConditionManager. Retreives telemetry property types and updates the | ||||
|          * select | ||||
|          * @private | ||||
|          */ | ||||
|         function onMetadataLoad() { | ||||
|             if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) { | ||||
|                 self.loadOptions(self.config.key); | ||||
|                 self.generateOptions(); | ||||
|             } | ||||
|             self.select.setSelected(self.config.operation); | ||||
|         } | ||||
|  | ||||
|         this.keySelect.on('change', onKeyChange); | ||||
|         this.manager.on('metadata', onMetadataLoad); | ||||
|  | ||||
|         if (this.manager.metadataLoadCompleted()) { | ||||
|             onMetadataLoad(); | ||||
|         } | ||||
|  | ||||
|         return this.select; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populate this select with options based on its current composition | ||||
|      */ | ||||
|     OperationSelect.prototype.generateOptions = function () { | ||||
|         var self = this, | ||||
|             items = this.operationKeys.map(function (operation) { | ||||
|                 return [operation, self.evaluator.getOperationText(operation)]; | ||||
|             }); | ||||
|         items.splice(0, 0, ['', NULLVALUE]); | ||||
|         this.select.setOptions(items); | ||||
|  | ||||
|         if (this.select.options.length < 2) { | ||||
|             this.select.hide(); | ||||
|         } else { | ||||
|             this.select.show(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the data type associated with a given telemetry property and | ||||
|      * the applicable operations from the {ConditionEvaluator} | ||||
|      * @param {string} key The telemetry property to load operations for | ||||
|      */ | ||||
|     OperationSelect.prototype.loadOptions = function (key) { | ||||
|         var self = this, | ||||
|             operations = self.evaluator.getOperationKeys(), | ||||
|             type; | ||||
|  | ||||
|         type = self.manager.getTelemetryPropertyType(self.config.object, key); | ||||
|  | ||||
|         self.operationKeys = operations.filter(function (operation) { | ||||
|             return self.evaluator.operationAppliesTo(operation, type); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     return OperationSelect; | ||||
|  | ||||
| }); | ||||
							
								
								
									
										166
									
								
								src/plugins/summaryWidget/src/input/Palette.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/plugins/summaryWidget/src/input/Palette.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| define([ | ||||
|     'text!../../res/input/paletteTemplate.html', | ||||
|     'EventEmitter', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     paletteTemplate, | ||||
|     EventEmitter, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Instantiates a new Open MCT Color Palette input | ||||
|      * @constructor | ||||
|      * @param {string} cssClass The class name of the icon which should be applied | ||||
|      *                          to this palette | ||||
|      * @param {Element} container The view that contains this palette | ||||
|      * @param {string[]} items A list of data items that will be associated with each | ||||
|      *                         palette item in the view; how this data is represented is | ||||
|      *                         up to the descendent class | ||||
|      */ | ||||
|     function Palette(cssClass, container, items) { | ||||
|         var self = this; | ||||
|  | ||||
|         this.cssClass = cssClass; | ||||
|         this.items = items; | ||||
|         this.container = container; | ||||
|  | ||||
|         this.domElement = $(paletteTemplate); | ||||
|         this.itemElements = { | ||||
|             nullOption: $('.l-option-row .s-palette-item', this.domElement) | ||||
|         }; | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['change']; | ||||
|         this.value = this.items[0]; | ||||
|         this.nullOption = ' '; | ||||
|  | ||||
|         this.hideMenu = this.hideMenu.bind(this); | ||||
|  | ||||
|         self.domElement.addClass(this.cssClass); | ||||
|         self.setNullOption(this.nullOption); | ||||
|  | ||||
|         $('.l-palette-row', self.domElement).after('<div class = "l-palette-row"> </div>'); | ||||
|         self.items.forEach(function (item) { | ||||
|             var itemElement = $('<div class = "l-palette-item s-palette-item"' + | ||||
|                                 ' data-item = ' + item + '> </div>'); | ||||
|             $('.l-palette-row:last-of-type', self.domElement).append(itemElement); | ||||
|             self.itemElements[item] = itemElement; | ||||
|         }); | ||||
|  | ||||
|         $('.menu', self.domElement).hide(); | ||||
|  | ||||
|         $(document).on('click', this.hideMenu); | ||||
|         $('.l-click-area', self.domElement).on('click', function (event) { | ||||
|             event.stopPropagation(); | ||||
|             $('.menu', self.container).hide(); | ||||
|             $('.menu', self.domElement).show(); | ||||
|         }); | ||||
|  | ||||
|         /** | ||||
|          * Event handler for selection of an individual palette item. Sets the | ||||
|          * currently selected element to be the one associated with that item's data | ||||
|          * @param {Event} event the click event that initiated this callback | ||||
|          * @private | ||||
|          */ | ||||
|         function handleItemClick(event) { | ||||
|             var elem = event.currentTarget, | ||||
|                 item = elem.dataset.item; | ||||
|             self.set(item); | ||||
|             $('.menu', self.domElement).hide(); | ||||
|         } | ||||
|  | ||||
|         $('.s-palette-item', self.domElement).on('click', handleItemClick); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the DOM element representing this palette in the view | ||||
|      */ | ||||
|     Palette.prototype.getDOM = function () { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Clean up any event listeners registered to DOM elements external to the widget | ||||
|      */ | ||||
|     Palette.prototype.destroy = function () { | ||||
|         $(document).off('click', this.hideMenu); | ||||
|     }; | ||||
|  | ||||
|     Palette.prototype.hideMenu = function () { | ||||
|         $('.menu', this.domElement).hide(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this palette: supported callback is change | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     Palette.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } else { | ||||
|             throw new Error('Unsupported event type: ' + event); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the currently selected value of this palette | ||||
|      * @return {string} The selected value | ||||
|      */ | ||||
|     Palette.prototype.getCurrent = function () { | ||||
|         return this.value; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Set the selected value of this palette; if the item doesn't exist in the | ||||
|      * palette's data model, the selected value will not change. Invokes any | ||||
|      * change callbacks associated with this palette. | ||||
|      * @param {string} item The key of the item to set as selected | ||||
|      */ | ||||
|     Palette.prototype.set = function (item) { | ||||
|         var self = this; | ||||
|         if (this.items.includes(item) || item === this.nullOption) { | ||||
|             this.value = item; | ||||
|             if (item === this.nullOption) { | ||||
|                 this.updateSelected('nullOption'); | ||||
|             } else { | ||||
|                 this.updateSelected(item); | ||||
|             } | ||||
|         } | ||||
|         this.eventEmitter.emit('change', self.value); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update the view assoicated with the currently selected item | ||||
|      */ | ||||
|     Palette.prototype.updateSelected = function (item) { | ||||
|         $('.s-palette-item', this.domElement).removeClass('selected'); | ||||
|         this.itemElements[item].addClass('selected'); | ||||
|         if (item === 'nullOption') { | ||||
|             $('.t-swatch', this.domElement).addClass('no-selection'); | ||||
|         } else { | ||||
|             $('.t-swatch', this.domElement).removeClass('no-selection'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * set the property to be used for the 'no selection' item. If not set, this | ||||
|      * defaults to a single space | ||||
|      * @param {string} item The key to use as the 'no selection' item | ||||
|      */ | ||||
|     Palette.prototype.setNullOption = function (item) { | ||||
|         this.nullOption = item; | ||||
|         this.itemElements.nullOption.data('item', item); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Hides the 'no selection' option to be hidden in the view if it doesn't apply | ||||
|      */ | ||||
|     Palette.prototype.toggleNullOption = function () { | ||||
|         $('.l-option-row', this.domElement).toggle(); | ||||
|     }; | ||||
|  | ||||
|     return Palette; | ||||
| }); | ||||
							
								
								
									
										144
									
								
								src/plugins/summaryWidget/src/input/Select.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/plugins/summaryWidget/src/input/Select.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| define([ | ||||
|     'text!../../res/input/selectTemplate.html', | ||||
|     'EventEmitter', | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     selectTemplate, | ||||
|     EventEmitter, | ||||
|     $ | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Wraps an HTML select element, and provides methods for dynamically altering | ||||
|      * its composition from the data model | ||||
|      * @constructor | ||||
|      */ | ||||
|     function Select() { | ||||
|         var self = this; | ||||
|  | ||||
|         this.domElement = $(selectTemplate); | ||||
|         this.options = []; | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.supportedCallbacks = ['change']; | ||||
|  | ||||
|         this.populate(); | ||||
|  | ||||
|         /** | ||||
|          * Event handler for the wrapped select element. Also invokes any change | ||||
|          * callbacks registered with this select with the new value | ||||
|          * @param {Event} event The change event that triggered this callback | ||||
|          * @private | ||||
|          */ | ||||
|         function onChange(event) { | ||||
|             var elem = event.target, | ||||
|                 value = self.options[$(elem).prop('selectedIndex')]; | ||||
|  | ||||
|             self.eventEmitter.emit('change', value[0]); | ||||
|         } | ||||
|  | ||||
|         $('select', this.domElement).on('change', onChange); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the DOM element representing this Select in the view | ||||
|      * @return {Element} | ||||
|      */ | ||||
|     Select.prototype.getDOM = function () { | ||||
|         return this.domElement; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a callback with this select: supported callback is change | ||||
|      * @param {string} event The key for the event to listen to | ||||
|      * @param {function} callback The function that this rule will envoke on this event | ||||
|      * @param {Object} context A reference to a scope to use as the context for | ||||
|      *                         context for the callback function | ||||
|      */ | ||||
|     Select.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } else { | ||||
|             throw new Error('Unsupported event type' + event); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update the select element in the view from the current state of the data | ||||
|      * model | ||||
|      */ | ||||
|     Select.prototype.populate = function () { | ||||
|         var self = this, | ||||
|             selectedIndex = 0; | ||||
|  | ||||
|         selectedIndex = $('select', this.domElement).prop('selectedIndex'); | ||||
|         $('option', this.domElement).remove(); | ||||
|  | ||||
|         self.options.forEach(function (option, index) { | ||||
|             $('select', self.domElement) | ||||
|                 .append('<option value = "' + option[0] + '"' + ' >' + | ||||
|                         option[1] + '</option>'); | ||||
|         }); | ||||
|  | ||||
|         $('select', this.domElement).prop('selectedIndex', selectedIndex); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Add a single option to this select | ||||
|      * @param {string} value The value for the new option | ||||
|      * @param {string} label The human-readable text for the new option | ||||
|      */ | ||||
|     Select.prototype.addOption = function (value, label) { | ||||
|         this.options.push([value, label]); | ||||
|         this.populate(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Set the available options for this select. Replaces any existing options | ||||
|      * @param {string[][]} options An array of [value, label] pairs to display | ||||
|      */ | ||||
|     Select.prototype.setOptions = function (options) { | ||||
|         this.options = options; | ||||
|         this.populate(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Sets the currently selected element an invokes any registered change | ||||
|      * callbacks with the new value. If the value doesn't exist in this select's | ||||
|      * model, its state will not change. | ||||
|      * @param {string} value The value to set as the selected option | ||||
|      */ | ||||
|     Select.prototype.setSelected = function (value) { | ||||
|         var selectedIndex = 0, | ||||
|             selectedOption; | ||||
|  | ||||
|         this.options.forEach (function (option, index) { | ||||
|             if (option[0] === value) { | ||||
|                 selectedIndex = index; | ||||
|             } | ||||
|         }); | ||||
|         $('select', this.domElement).prop('selectedIndex', selectedIndex); | ||||
|  | ||||
|         selectedOption = this.options[selectedIndex]; | ||||
|         this.eventEmitter.emit('change', selectedOption[0]); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the value of the currently selected item | ||||
|      * @return {string} | ||||
|      */ | ||||
|     Select.prototype.getSelected = function () { | ||||
|         return $('select', this.domElement).prop('value'); | ||||
|     }; | ||||
|  | ||||
|     Select.prototype.hide = function () { | ||||
|         $(this.domElement).addClass('hidden'); | ||||
|         $('.equal-to').addClass('hidden'); | ||||
|     }; | ||||
|  | ||||
|     Select.prototype.show = function () { | ||||
|         $(this.domElement).removeClass('hidden'); | ||||
|         $('.equal-to').removeClass('hidden'); | ||||
|     }; | ||||
|  | ||||
|     return Select; | ||||
| }); | ||||
							
								
								
									
										337
									
								
								src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,337 @@ | ||||
| define(['../src/ConditionEvaluator'], function (ConditionEvaluator) { | ||||
|     describe('A Summary Widget Rule Evaluator', function () { | ||||
|         var evaluator, | ||||
|             testEvaluator, | ||||
|             testOperation, | ||||
|             mockCache, | ||||
|             mockTestCache, | ||||
|             mockComposition, | ||||
|             mockConditions, | ||||
|             mockConditionsEmpty, | ||||
|             mockConditionsUndefined, | ||||
|             mockConditionsAnyTrue, | ||||
|             mockConditionsAllTrue, | ||||
|             mockConditionsAnyFalse, | ||||
|             mockConditionsAllFalse, | ||||
|             mockOperations; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockCache = { | ||||
|                 a: { | ||||
|                     alpha: 3, | ||||
|                     beta: 9, | ||||
|                     gamma: 'Testing 1 2 3' | ||||
|                 }, | ||||
|                 b: { | ||||
|                     alpha: 44, | ||||
|                     beta: 23, | ||||
|                     gamma: 'Hello World' | ||||
|                 }, | ||||
|                 c: { | ||||
|                     foo: 'bar', | ||||
|                     iAm: 'The Walrus', | ||||
|                     creature: { | ||||
|                         type: 'Centaur' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             mockTestCache = { | ||||
|                 a: { | ||||
|                     alpha: 1, | ||||
|                     beta: 1, | ||||
|                     gamma: 'Testing 4 5 6' | ||||
|                 }, | ||||
|                 b: { | ||||
|                     alpha: 2, | ||||
|                     beta: 2, | ||||
|                     gamma: 'Goodbye world' | ||||
|                 } | ||||
|             }; | ||||
|             mockComposition = { | ||||
|                 a: {}, | ||||
|                 b: {}, | ||||
|                 c: {} | ||||
|             }; | ||||
|             mockConditions = [{ | ||||
|                 object: 'a', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'greaterThan', | ||||
|                 values: [2] | ||||
|             },{ | ||||
|                 object: 'b', | ||||
|                 key: 'gamma', | ||||
|                 operation: 'lessThan', | ||||
|                 values: [5] | ||||
|             }]; | ||||
|             mockConditionsEmpty = [{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             }]; | ||||
|             mockConditionsUndefined = [{ | ||||
|                 object: 'No Such Object', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'a', | ||||
|                 key: 'No Such Key', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'a', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'No Such Operation', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'all', | ||||
|                 key: 'Nonexistent Field', | ||||
|                 operation: 'Random Operation', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'any', | ||||
|                 key: 'Nonexistent Field', | ||||
|                 operation: 'Whatever Operation', | ||||
|                 values: [] | ||||
|             }]; | ||||
|             mockConditionsAnyTrue = [{ | ||||
|                 object: 'any', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'greaterThan', | ||||
|                 values: [5] | ||||
|             }]; | ||||
|             mockConditionsAnyFalse = [{ | ||||
|                 object: 'any', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'greaterThan', | ||||
|                 values: [1000] | ||||
|             }]; | ||||
|             mockConditionsAllFalse = [{ | ||||
|                 object: 'all', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'greaterThan', | ||||
|                 values: [5] | ||||
|             }]; | ||||
|             mockConditionsAllTrue = [{ | ||||
|                 object: 'all', | ||||
|                 key: 'alpha', | ||||
|                 operation: 'greaterThan', | ||||
|                 values: [0] | ||||
|             }]; | ||||
|             mockOperations = { | ||||
|                 greaterThan: { | ||||
|                     operation: function (input) { | ||||
|                         return input[0] > input[1]; | ||||
|                     }, | ||||
|                     text: 'is greater than', | ||||
|                     appliesTo: ['number'], | ||||
|                     inputCount: 1, | ||||
|                     getDescription: function (values) { | ||||
|                         return ' > ' + values [0]; | ||||
|                     } | ||||
|                 }, | ||||
|                 lessThan: { | ||||
|                     operation: function (input) { | ||||
|                         return input[0] < input[1]; | ||||
|                     }, | ||||
|                     text: 'is less than', | ||||
|                     appliesTo: ['number'], | ||||
|                     inputCount: 1 | ||||
|                 }, | ||||
|                 textContains: { | ||||
|                     operation: function (input) { | ||||
|                         return input[0] && input[1] && input[0].includes(input[1]); | ||||
|                     }, | ||||
|                     text: 'text contains', | ||||
|                     appliesTo: ['string'], | ||||
|                     inputCount: 1 | ||||
|                 }, | ||||
|                 textIsExactly: { | ||||
|                     operation: function (input) { | ||||
|                         return input[0] === input[1]; | ||||
|                     }, | ||||
|                     text: 'text is exactly', | ||||
|                     appliesTo: ['string'], | ||||
|                     inputCount: 1 | ||||
|                 }, | ||||
|                 isHalfHorse: { | ||||
|                     operation: function (input) { | ||||
|                         return input[0].type === 'Centaur'; | ||||
|                     }, | ||||
|                     text: 'is Half Horse', | ||||
|                     appliesTo: ['mythicalCreature'], | ||||
|                     inputCount: 0, | ||||
|                     getDescription: function () { | ||||
|                         return 'is half horse'; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             evaluator = new ConditionEvaluator(mockCache, mockComposition); | ||||
|             testEvaluator = new ConditionEvaluator(mockCache, mockComposition); | ||||
|             evaluator.operations = mockOperations; | ||||
|         }); | ||||
|  | ||||
|         it('evaluates a condition when it has no configuration', function () { | ||||
|             expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); | ||||
|             expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('correctly evaluates a set of conditions', function () { | ||||
|             expect(evaluator.execute(mockConditions, 'any')).toEqual(true); | ||||
|             expect(evaluator.execute(mockConditions, 'all')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('correctly evaluates conditions involving "any telemetry"', function () { | ||||
|             expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); | ||||
|             expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('correctly evaluates conditions involving "all telemetry"', function () { | ||||
|             expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); | ||||
|             expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('handles malformed conditions gracefully', function () { | ||||
|             //if no conditions are fully defined, should return false for any mode | ||||
|             expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); | ||||
|             expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); | ||||
|             expect(evaluator.execute(mockConditionsUndefined, 'js')).toEqual(false); | ||||
|             //these conditions are true: evaluator should ignore undefined conditions, | ||||
|             //and evaluate the rule as true | ||||
|             mockConditionsUndefined.push({ | ||||
|                 object: 'a', | ||||
|                 key: 'gamma', | ||||
|                 operation: 'textContains', | ||||
|                 values: ['Testing'] | ||||
|             }); | ||||
|             expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); | ||||
|             mockConditionsUndefined.push({ | ||||
|                 object: 'c', | ||||
|                 key: 'iAm', | ||||
|                 operation: 'textContains', | ||||
|                 values: ['Walrus'] | ||||
|             }); | ||||
|             expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); | ||||
|         }); | ||||
|  | ||||
|         it('gets the keys for possible operations', function () { | ||||
|             expect(evaluator.getOperationKeys()).toEqual( | ||||
|               ['greaterThan', 'lessThan', 'textContains', 'textIsExactly', 'isHalfHorse'] | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|         it('gets output text for a given operation', function () { | ||||
|             expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse'); | ||||
|         }); | ||||
|  | ||||
|         it('correctly returns whether an operation applies to a given type', function () { | ||||
|             expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true); | ||||
|             expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('returns the HTML input type associated with a given data type', function () { | ||||
|             expect(evaluator.getInputTypeById('string')).toEqual('text'); | ||||
|         }); | ||||
|  | ||||
|         it('gets the number of inputs required for a given operation', function () { | ||||
|             expect(evaluator.getInputCount('isHalfHorse')).toEqual(0); | ||||
|             expect(evaluator.getInputCount('greaterThan')).toEqual(1); | ||||
|         }); | ||||
|  | ||||
|         it('gets a human-readable description of a condition', function () { | ||||
|             expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse'); | ||||
|             expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1'); | ||||
|         }); | ||||
|  | ||||
|         it('allows setting a substitute cache for testing purposes, and toggling its use', function () { | ||||
|             evaluator.setTestDataCache(mockTestCache); | ||||
|             evaluator.useTestData(true); | ||||
|             expect(evaluator.execute(mockConditions, 'any')).toEqual(false); | ||||
|             expect(evaluator.execute(mockConditions, 'all')).toEqual(false); | ||||
|             mockConditions.push({ | ||||
|                 object: 'a', | ||||
|                 key: 'gamma', | ||||
|                 operation: 'textContains', | ||||
|                 values: ['4 5 6'] | ||||
|             }); | ||||
|             expect(evaluator.execute(mockConditions, 'any')).toEqual(true); | ||||
|             expect(evaluator.execute(mockConditions, 'all')).toEqual(false); | ||||
|             mockConditions.pop(); | ||||
|             evaluator.useTestData(false); | ||||
|             expect(evaluator.execute(mockConditions, 'any')).toEqual(true); | ||||
|             expect(evaluator.execute(mockConditions, 'all')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('supports all required operations', function () { | ||||
|             //equal to | ||||
|             testOperation = testEvaluator.operations.equalTo.operation; | ||||
|             expect(testOperation([33, 33])).toEqual(true); | ||||
|             expect(testOperation([55, 147])).toEqual(false); | ||||
|             //not equal to | ||||
|             testOperation = testEvaluator.operations.notEqualTo.operation; | ||||
|             expect(testOperation([33, 33])).toEqual(false); | ||||
|             expect(testOperation([55, 147])).toEqual(true); | ||||
|             //greater than | ||||
|             testOperation = testEvaluator.operations.greaterThan.operation; | ||||
|             expect(testOperation([100, 33])).toEqual(true); | ||||
|             expect(testOperation([33, 33])).toEqual(false); | ||||
|             expect(testOperation([55, 147])).toEqual(false); | ||||
|             //less than | ||||
|             testOperation = testEvaluator.operations.lessThan.operation; | ||||
|             expect(testOperation([100, 33])).toEqual(false); | ||||
|             expect(testOperation([33, 33])).toEqual(false); | ||||
|             expect(testOperation([55, 147])).toEqual(true); | ||||
|             //greater than or equal to | ||||
|             testOperation = testEvaluator.operations.greaterThanOrEq.operation; | ||||
|             expect(testOperation([100, 33])).toEqual(true); | ||||
|             expect(testOperation([33, 33])).toEqual(true); | ||||
|             expect(testOperation([55, 147])).toEqual(false); | ||||
|             //less than or equal to | ||||
|             testOperation = testEvaluator.operations.lessThanOrEq.operation; | ||||
|             expect(testOperation([100, 33])).toEqual(false); | ||||
|             expect(testOperation([33, 33])).toEqual(true); | ||||
|             expect(testOperation([55, 147])).toEqual(true); | ||||
|             //between | ||||
|             testOperation = testEvaluator.operations.between.operation; | ||||
|             expect(testOperation([100, 33, 66])).toEqual(false); | ||||
|             expect(testOperation([1, 33, 66])).toEqual(false); | ||||
|             expect(testOperation([45, 33, 66])).toEqual(true); | ||||
|             //not between | ||||
|             testOperation = testEvaluator.operations.notBetween.operation; | ||||
|             expect(testOperation([100, 33, 66])).toEqual(true); | ||||
|             expect(testOperation([1, 33, 66])).toEqual(true); | ||||
|             expect(testOperation([45, 33, 66])).toEqual(false); | ||||
|             //text contains | ||||
|             testOperation = testEvaluator.operations.textContains.operation; | ||||
|             expect(testOperation(['Testing', 'tin'])).toEqual(true); | ||||
|             expect(testOperation(['Testing', 'bind'])).toEqual(false); | ||||
|             //text does not contain | ||||
|             testOperation = testEvaluator.operations.textDoesNotContain.operation; | ||||
|             expect(testOperation(['Testing', 'tin'])).toEqual(false); | ||||
|             expect(testOperation(['Testing', 'bind'])).toEqual(true); | ||||
|             //text starts with | ||||
|             testOperation = testEvaluator.operations.textStartsWith.operation; | ||||
|             expect(testOperation(['Testing', 'Tes'])).toEqual(true); | ||||
|             expect(testOperation(['Testing', 'ting'])).toEqual(false); | ||||
|             //text ends with | ||||
|             testOperation = testEvaluator.operations.textEndsWith.operation; | ||||
|             expect(testOperation(['Testing', 'Tes'])).toEqual(false); | ||||
|             expect(testOperation(['Testing', 'ting'])).toEqual(true); | ||||
|             //text is exactly | ||||
|             testOperation = testEvaluator.operations.textIsExactly.operation; | ||||
|             expect(testOperation(['Testing', 'Testing'])).toEqual(true); | ||||
|             expect(testOperation(['Testing', 'Test'])).toEqual(false); | ||||
|             //undefined | ||||
|             testOperation = testEvaluator.operations.isUndefined.operation; | ||||
|             expect(testOperation([1])).toEqual(false); | ||||
|             expect(testOperation([])).toEqual(true); | ||||
|         }); | ||||
|  | ||||
|         it('can produce a description for all supported operations', function () { | ||||
|             testEvaluator.getOperationKeys().forEach(function (key) { | ||||
|                 expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										372
									
								
								src/plugins/summaryWidget/test/ConditionManagerSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								src/plugins/summaryWidget/test/ConditionManagerSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,372 @@ | ||||
| define(['../src/ConditionManager'], function (ConditionManager) { | ||||
|     describe('A Summary Widget Condition Manager', function () { | ||||
|         var conditionManager, | ||||
|             mockDomainObject, | ||||
|             mockCompObject1, | ||||
|             mockCompObject2, | ||||
|             mockCompObject3, | ||||
|             mockMetadata, | ||||
|             mockTelemetryCallbacks, | ||||
|             mockEventCallbacks, | ||||
|             unsubscribeSpies, | ||||
|             unregisterSpies, | ||||
|             mockMetadataManagers, | ||||
|             mockComposition, | ||||
|             mockOpenMCT, | ||||
|             mockTelemetryAPI, | ||||
|             addCallbackSpy, | ||||
|             loadCallbackSpy, | ||||
|             removeCallbackSpy, | ||||
|             telemetryCallbackSpy, | ||||
|             metadataCallbackSpy, | ||||
|             mockTelemetryValues, | ||||
|             mockTelemetryValues2, | ||||
|             mockConditionEvaluator; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'testKey' | ||||
|                 }, | ||||
|                 name: 'Test Object', | ||||
|                 composition: [{ | ||||
|                     mockCompObject1: { | ||||
|                         key: 'mockCompObject1' | ||||
|                     }, | ||||
|                     mockCompObject2 : { | ||||
|                         key: 'mockCompObject2' | ||||
|                     } | ||||
|                 }], | ||||
|                 configuration: {} | ||||
|             }; | ||||
|             mockCompObject1 = { | ||||
|                 identifier: { | ||||
|                     key: 'mockCompObject1' | ||||
|                 }, | ||||
|                 name: 'Object 1' | ||||
|             }; | ||||
|             mockCompObject2 = { | ||||
|                 identifier: { | ||||
|                     key: 'mockCompObject2' | ||||
|                 }, | ||||
|                 name: 'Object 2' | ||||
|             }; | ||||
|             mockCompObject3 = { | ||||
|                 identifier: { | ||||
|                     key: 'mockCompObject3' | ||||
|                 }, | ||||
|                 name: 'Object 3' | ||||
|             }; | ||||
|             mockMetadata = { | ||||
|                 mockCompObject1: { | ||||
|                     property1: { | ||||
|                         key: 'property1', | ||||
|                         name: 'Property 1' | ||||
|                     }, | ||||
|                     property2: { | ||||
|                         key: 'property2', | ||||
|                         name: 'Property 2' | ||||
|                     } | ||||
|                 }, | ||||
|                 mockCompObject2: { | ||||
|                     property3: { | ||||
|                         key: 'property3', | ||||
|                         name: 'Property 3' | ||||
|                     }, | ||||
|                     property4: { | ||||
|                         key: 'property4', | ||||
|                         name: 'Property 4' | ||||
|                     } | ||||
|                 }, | ||||
|                 mockCompObject3: { | ||||
|                     property1: { | ||||
|                         key: 'property1', | ||||
|                         name: 'Property 1' | ||||
|                     }, | ||||
|                     property2: { | ||||
|                         key: 'property2', | ||||
|                         name: 'Property 2' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             mockTelemetryCallbacks = {}; | ||||
|             mockEventCallbacks = {}; | ||||
|             unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ | ||||
|                 'mockCompObject1', | ||||
|                 'mockCompObject2', | ||||
|                 'mockCompObject3' | ||||
|             ]); | ||||
|             unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', [ | ||||
|                 'load', | ||||
|                 'remove', | ||||
|                 'add' | ||||
|             ]); | ||||
|             mockTelemetryValues = { | ||||
|                 mockCompObject1: { | ||||
|                     property1: 'Its a string', | ||||
|                     property2: 42 | ||||
|                 }, | ||||
|                 mockCompObject2: { | ||||
|                     property3: 'Execute order:', | ||||
|                     property4: 66 | ||||
|                 }, | ||||
|                 mockCompObject3: { | ||||
|                     property1: 'Testing 1 2 3', | ||||
|                     property2: 9000 | ||||
|                 } | ||||
|             }; | ||||
|             mockTelemetryValues2 = { | ||||
|                 mockCompObject1: { | ||||
|                     property1: 'Its a different string', | ||||
|                     property2: 44 | ||||
|                 }, | ||||
|                 mockCompObject2: { | ||||
|                     property3: 'Execute catch:', | ||||
|                     property4: 22 | ||||
|                 }, | ||||
|                 mockCompObject3: { | ||||
|                     property1: 'Walrus', | ||||
|                     property2: 22 | ||||
|                 } | ||||
|             }; | ||||
|             mockMetadataManagers = { | ||||
|                 mockCompObject1: { | ||||
|                     values: jasmine.createSpy('metadataManager').andReturn( | ||||
|                         Object.values(mockMetadata.mockCompObject1) | ||||
|                     ) | ||||
|                 }, | ||||
|                 mockCompObject2: { | ||||
|                     values: jasmine.createSpy('metadataManager').andReturn( | ||||
|                         Object.values(mockMetadata.mockCompObject2) | ||||
|                     ) | ||||
|                 }, | ||||
|                 mockCompObject3: { | ||||
|                     values: jasmine.createSpy('metadataManager').andReturn( | ||||
|                         Object.values(mockMetadata.mockCompObject2) | ||||
|                     ) | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockComposition = jasmine.createSpyObj('composition', [ | ||||
|                 'on', | ||||
|                 'off', | ||||
|                 'load', | ||||
|                 'triggerCallback' | ||||
|             ]); | ||||
|             mockComposition.on.andCallFake(function (event, callback, context) { | ||||
|                 mockEventCallbacks[event] = callback.bind(context); | ||||
|             }); | ||||
|             mockComposition.off.andCallFake(function (event) { | ||||
|                 unregisterSpies[event](); | ||||
|             }); | ||||
|             mockComposition.load.andCallFake(function () { | ||||
|                 mockEventCallbacks.add(mockCompObject1); | ||||
|                 mockEventCallbacks.add(mockCompObject2); | ||||
|                 mockEventCallbacks.load(); | ||||
|             }); | ||||
|             mockComposition.triggerCallback.andCallFake(function (event) { | ||||
|                 if (event === 'add') { | ||||
|                     mockEventCallbacks.add(mockCompObject3); | ||||
|                 } else if (event === 'remove') { | ||||
|                     mockEventCallbacks.remove({ | ||||
|                         key: 'mockCompObject2' | ||||
|                     }); | ||||
|                 } else { | ||||
|                     mockEventCallbacks[event](); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ | ||||
|                 'request', | ||||
|                 'canProvideTelemetry', | ||||
|                 'getMetadata', | ||||
|                 'subscribe', | ||||
|                 'triggerTelemetryCallback' | ||||
|             ]); | ||||
|             mockTelemetryAPI.request.andCallFake(function (obj) { | ||||
|                 return new Promise(function (resolve, reject) { | ||||
|                     resolve(mockTelemetryValues[obj.identifer.key]); | ||||
|                 }); | ||||
|             }); | ||||
|             mockTelemetryAPI.canProvideTelemetry.andReturn(true); | ||||
|             mockTelemetryAPI.getMetadata.andCallFake(function (obj) { | ||||
|                 return mockMetadataManagers[obj.identifier.key]; | ||||
|             }); | ||||
|             mockTelemetryAPI.subscribe.andCallFake(function (obj, callback) { | ||||
|                 mockTelemetryCallbacks[obj.identifier.key] = callback; | ||||
|                 return unsubscribeSpies[obj.identifier.key]; | ||||
|             }); | ||||
|             mockTelemetryAPI.triggerTelemetryCallback.andCallFake(function (key) { | ||||
|                 mockTelemetryCallbacks[key](mockTelemetryValues2[key]); | ||||
|             }); | ||||
|  | ||||
|             mockOpenMCT = { | ||||
|                 telemetry: mockTelemetryAPI, | ||||
|                 composition: {} | ||||
|             }; | ||||
|             mockOpenMCT.composition.get = jasmine.createSpy('get').andReturn(mockComposition); | ||||
|  | ||||
|             loadCallbackSpy = jasmine.createSpy('loadCallbackSpy'); | ||||
|             addCallbackSpy = jasmine.createSpy('addCallbackSpy'); | ||||
|             removeCallbackSpy = jasmine.createSpy('removeCallbackSpy'); | ||||
|             metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy'); | ||||
|             telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy'); | ||||
|  | ||||
|             conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT); | ||||
|             conditionManager.on('load', loadCallbackSpy); | ||||
|             conditionManager.on('add', addCallbackSpy); | ||||
|             conditionManager.on('remove', removeCallbackSpy); | ||||
|             conditionManager.on('metadata', metadataCallbackSpy); | ||||
|             conditionManager.on('receiveTelemetry', telemetryCallbackSpy); | ||||
|  | ||||
|             mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator'); | ||||
|             mockConditionEvaluator.execute = jasmine.createSpy('execute'); | ||||
|             conditionManager.evaluator = mockConditionEvaluator; | ||||
|         }); | ||||
|  | ||||
|         it('loads the initial composition and invokes the appropriate handlers', function () { | ||||
|             mockComposition.triggerCallback('load'); | ||||
|             expect(conditionManager.getComposition()).toEqual({ | ||||
|                 mockCompObject1: mockCompObject1, | ||||
|                 mockCompObject2: mockCompObject2 | ||||
|             }); | ||||
|             expect(loadCallbackSpy).toHaveBeenCalled(); | ||||
|             expect(conditionManager.loadCompleted()).toEqual(true); | ||||
|         }); | ||||
|  | ||||
|         it('loads metadata from composition and gets it upon request', function () { | ||||
|             expect(conditionManager.getTelemetryMetadata('mockCompObject1')) | ||||
|                 .toEqual(mockMetadata.mockCompObject1); | ||||
|             expect(conditionManager.getTelemetryMetadata('mockCompObject2')) | ||||
|                 .toEqual(mockMetadata.mockCompObject2); | ||||
|         }); | ||||
|  | ||||
|         it('maintains lists of global metadata, and does not duplicate repeated fields', function () { | ||||
|             var allKeys = { | ||||
|                 property1: { | ||||
|                     key: 'property1', | ||||
|                     name: 'Property 1' | ||||
|                 }, | ||||
|                 property2: { | ||||
|                     key: 'property2', | ||||
|                     name: 'Property 2' | ||||
|                 }, | ||||
|                 property3: { | ||||
|                     key: 'property3', | ||||
|                     name: 'Property 3' | ||||
|                 }, | ||||
|                 property4: { | ||||
|                     key: 'property4', | ||||
|                     name: 'Property 4' | ||||
|                 } | ||||
|             }; | ||||
|             expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); | ||||
|             expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); | ||||
|             mockComposition.triggerCallback('add'); | ||||
|             expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); | ||||
|             expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); | ||||
|         }); | ||||
|  | ||||
|         it('loads and gets telemetry property types', function () { | ||||
|             conditionManager.parseAllPropertyTypes().then(function () { | ||||
|                 expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')) | ||||
|                     .toEqual('string'); | ||||
|                 expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')) | ||||
|                     .toEqual('number'); | ||||
|                 expect(conditionManager.metadataLoadComplete()).toEqual(true); | ||||
|                 expect(metadataCallbackSpy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a composition add event and invokes the appropriate handlers', function () { | ||||
|             mockComposition.triggerCallback('add'); | ||||
|             expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); | ||||
|             expect(conditionManager.getComposition()).toEqual({ | ||||
|                 mockCompObject1: mockCompObject1, | ||||
|                 mockCompObject2: mockCompObject2, | ||||
|                 mockCompObject3: mockCompObject3 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a composition remove event and invokes the appropriate handlers', function () { | ||||
|             mockComposition.triggerCallback('remove'); | ||||
|             expect(removeCallbackSpy).toHaveBeenCalledWith({ | ||||
|                 key: 'mockCompObject2' | ||||
|             }); | ||||
|             expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled(); | ||||
|             expect(conditionManager.getComposition()).toEqual({ | ||||
|                 mockCompObject1: mockCompObject1 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('unregisters telemetry subscriptions and composition listeners on destroy', function () { | ||||
|             mockComposition.triggerCallback('add'); | ||||
|             conditionManager.destroy(); | ||||
|             Object.values(unsubscribeSpies).forEach(function (spy) { | ||||
|                 expect(spy).toHaveBeenCalled(); | ||||
|             }); | ||||
|             Object.values(unregisterSpies).forEach(function (spy) { | ||||
|                 expect(spy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('populates its LAD cache with historial data on load, if available', function () { | ||||
|             conditionManager.parseAllPropertyTypes().then(function () { | ||||
|                 expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a string'); | ||||
|                 expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('updates its LAD cache upon recieving telemetry and invokes the appropriate handlers', function () { | ||||
|             mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); | ||||
|             expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string'); | ||||
|             mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2'); | ||||
|             expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22); | ||||
|             expect(telemetryCallbackSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('evalutes a set of rules and returns the id of the' + | ||||
|            'last active rule, or the first if no rules are active', function () { | ||||
|             var mockRuleOrder = ['default', 'rule0', 'rule1'], | ||||
|                 mockRules = { | ||||
|                     default: { | ||||
|                         getProperty: function () {} | ||||
|                     }, | ||||
|                     rule0: { | ||||
|                         getProperty: function () {} | ||||
|                     }, | ||||
|                     rule1: { | ||||
|                         getProperty: function () {} | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|             mockConditionEvaluator.execute.andReturn(false); | ||||
|             expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default'); | ||||
|             mockConditionEvaluator.execute.andReturn(true); | ||||
|             expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1'); | ||||
|         }); | ||||
|  | ||||
|         it('gets the human-readable name of a composition object', function () { | ||||
|             expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); | ||||
|             expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); | ||||
|         }); | ||||
|  | ||||
|         it('gets the human-readable name of a telemetry field', function () { | ||||
|             conditionManager.parseAllPropertyTypes().then(function () { | ||||
|                 expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')) | ||||
|                     .toEqual('Property 1'); | ||||
|                 expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')) | ||||
|                     .toEqual('Property 4'); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('gets its associated ConditionEvaluator', function () { | ||||
|             expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); | ||||
|         }); | ||||
|  | ||||
|         it('allows forcing a receive telemetry event', function () { | ||||
|             conditionManager.triggerTelemetryCallback(); | ||||
|             expect(telemetryCallbackSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										157
									
								
								src/plugins/summaryWidget/test/ConditionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/plugins/summaryWidget/test/ConditionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| define(['../src/Condition', 'zepto'], function (Condition, $) { | ||||
|     describe('A summary widget condition', function () { | ||||
|         var testCondition, | ||||
|             mockConfig, | ||||
|             mockConditionManager, | ||||
|             mockContainer, | ||||
|             mockEvaluator, | ||||
|             changeSpy, | ||||
|             duplicateSpy, | ||||
|             removeSpy, | ||||
|             generateValuesSpy; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockContainer = $(document.createElement('div')); | ||||
|  | ||||
|             mockConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'property1', | ||||
|                 operation: 'operation1', | ||||
|                 values: [1, 2, 3] | ||||
|             }; | ||||
|  | ||||
|             mockEvaluator = {}; | ||||
|             mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); | ||||
|             mockEvaluator.getInputType = jasmine.createSpy('inputType'); | ||||
|  | ||||
|             mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ | ||||
|                 'on', | ||||
|                 'getComposition', | ||||
|                 'loadCompleted', | ||||
|                 'getEvaluator', | ||||
|                 'getTelemetryMetadata', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'getObjectName', | ||||
|                 'getTelemetryPropertyName' | ||||
|             ]); | ||||
|             mockConditionManager.loadCompleted.andReturn(false); | ||||
|             mockConditionManager.metadataLoadCompleted.andReturn(false); | ||||
|             mockConditionManager.getEvaluator.andReturn(mockEvaluator); | ||||
|             mockConditionManager.getComposition.andReturn({}); | ||||
|             mockConditionManager.getTelemetryMetadata.andReturn({}); | ||||
|             mockConditionManager.getObjectName.andReturn('Object Name'); | ||||
|             mockConditionManager.getTelemetryPropertyName.andReturn('Property Name'); | ||||
|  | ||||
|             duplicateSpy = jasmine.createSpy('duplicate'); | ||||
|             removeSpy = jasmine.createSpy('remove'); | ||||
|             changeSpy = jasmine.createSpy('change'); | ||||
|             generateValuesSpy = jasmine.createSpy('generateValueInputs'); | ||||
|  | ||||
|             testCondition = new Condition(mockConfig, 54, mockConditionManager); | ||||
|  | ||||
|             testCondition.on('duplicate', duplicateSpy); | ||||
|             testCondition.on('remove', removeSpy); | ||||
|             testCondition.on('change', changeSpy); | ||||
|         }); | ||||
|  | ||||
|         it('exposes a DOM element to represent itself in the view', function () { | ||||
|             mockContainer.append(testCondition.getDOM()); | ||||
|             expect($('.t-condition', mockContainer).get().length).toEqual(1); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its object select', function () { | ||||
|             testCondition.selects.object.setSelected(''); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: '', | ||||
|                 property: 'object', | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its key select', function () { | ||||
|             testCondition.selects.key.setSelected(''); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: '', | ||||
|                 property: 'key', | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its operation select', function () { | ||||
|             testCondition.generateValueInputs = generateValuesSpy; | ||||
|             testCondition.selects.operation.setSelected(''); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: '', | ||||
|                 property: 'operation', | ||||
|                 index: 54 | ||||
|             }); | ||||
|             expect(generateValuesSpy).toHaveBeenCalledWith(''); | ||||
|         }); | ||||
|  | ||||
|         it('generates value inputs of the appropriate type and quantity', function () { | ||||
|             mockContainer.append(testCondition.getDOM()); | ||||
|             mockEvaluator.getInputType.andReturn('number'); | ||||
|             mockEvaluator.getInputCount.andReturn(3); | ||||
|             testCondition.generateValueInputs(''); | ||||
|             expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(3); | ||||
|             expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(1); | ||||
|             expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(2); | ||||
|             expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(3); | ||||
|  | ||||
|             mockEvaluator.getInputType.andReturn('text'); | ||||
|             mockEvaluator.getInputCount.andReturn(2); | ||||
|             testCondition.config.values = ['Text I Am', 'Text It Is']; | ||||
|             testCondition.generateValueInputs(''); | ||||
|             expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(2); | ||||
|             expect($('input', mockContainer).eq(0).prop('value')).toEqual('Text I Am'); | ||||
|             expect($('input', mockContainer).eq(1).prop('value')).toEqual('Text It Is'); | ||||
|         }); | ||||
|  | ||||
|         it('ensures reasonable defaults on values if none are provided', function () { | ||||
|             mockContainer.append(testCondition.getDOM()); | ||||
|             mockEvaluator.getInputType.andReturn('number'); | ||||
|             mockEvaluator.getInputCount.andReturn(3); | ||||
|             testCondition.config.values = []; | ||||
|             testCondition.generateValueInputs(''); | ||||
|             expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(0); | ||||
|             expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(0); | ||||
|             expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(0); | ||||
|             expect(testCondition.config.values).toEqual([0, 0, 0]); | ||||
|  | ||||
|             mockEvaluator.getInputType.andReturn('text'); | ||||
|             mockEvaluator.getInputCount.andReturn(2); | ||||
|             testCondition.config.values = []; | ||||
|             testCondition.generateValueInputs(''); | ||||
|             expect($('input', mockContainer).eq(0).prop('value')).toEqual(''); | ||||
|             expect($('input', mockContainer).eq(1).prop('value')).toEqual(''); | ||||
|             expect(testCondition.config.values).toEqual(['', '']); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its value inputs', function () { | ||||
|             mockContainer.append(testCondition.getDOM()); | ||||
|             mockEvaluator.getInputType.andReturn('number'); | ||||
|             mockEvaluator.getInputCount.andReturn(3); | ||||
|             testCondition.generateValueInputs(''); | ||||
|             $('input', mockContainer).eq(1).prop('value', 9001); | ||||
|             $('input', mockContainer).eq(1).trigger('input'); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: 9001, | ||||
|                 property: 'values[1]', | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('can remove itself from the configuration', function () { | ||||
|             testCondition.remove(); | ||||
|             expect(removeSpy).toHaveBeenCalledWith(54); | ||||
|         }); | ||||
|  | ||||
|         it('can duplicate itself', function () { | ||||
|             testCondition.duplicate(); | ||||
|             expect(duplicateSpy).toHaveBeenCalledWith({ | ||||
|                 sourceCondition: mockConfig, | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										269
									
								
								src/plugins/summaryWidget/test/RuleSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/plugins/summaryWidget/test/RuleSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| define(['../src/Rule', 'zepto'], function (Rule, $) { | ||||
|     describe('A Summary Widget Rule', function () { | ||||
|         var mockRuleConfig, | ||||
|             mockDomainObject, | ||||
|             mockOpenMCT, | ||||
|             mockConditionManager, | ||||
|             mockWidgetDnD, | ||||
|             mockEvaluator, | ||||
|             mockContainer, | ||||
|             testRule, | ||||
|             removeSpy, | ||||
|             duplicateSpy, | ||||
|             changeSpy, | ||||
|             conditionChangeSpy; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockRuleConfig = { | ||||
|                 name: 'Name', | ||||
|                 id: 'mockRule', | ||||
|                 icon: 'test-icon-name', | ||||
|                 style: { | ||||
|                     'background-color': '', | ||||
|                     'border-color': '', | ||||
|                     'color': '' | ||||
|                 }, | ||||
|                 expanded: true, | ||||
|                 conditions: [{ | ||||
|                     object: '', | ||||
|                     key: '', | ||||
|                     operation: '', | ||||
|                     values: [] | ||||
|                 },{ | ||||
|                     object: 'blah', | ||||
|                     key: 'blah', | ||||
|                     operation: 'blah', | ||||
|                     values: ['blah.', 'blah!', 'blah?'] | ||||
|                 }] | ||||
|             }; | ||||
|             mockDomainObject = { | ||||
|                 configuration: { | ||||
|                     ruleConfigById: { | ||||
|                         mockRule: mockRuleConfig, | ||||
|                         otherRule: {} | ||||
|                     }, | ||||
|                     ruleOrder: ['default', 'mockRule', 'otherRule'] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockOpenMCT = {}; | ||||
|             mockOpenMCT.objects = {}; | ||||
|             mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); | ||||
|  | ||||
|             mockEvaluator = {}; | ||||
|             mockEvaluator.getOperationDescription = jasmine.createSpy('evaluator') | ||||
|                                                         .andReturn('Operation Description'); | ||||
|  | ||||
|             mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ | ||||
|                 'on', | ||||
|                 'getComposition', | ||||
|                 'loadCompleted', | ||||
|                 'getEvaluator', | ||||
|                 'getTelemetryMetadata', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'getObjectName', | ||||
|                 'getTelemetryPropertyName' | ||||
|             ]); | ||||
|             mockConditionManager.loadCompleted.andReturn(false); | ||||
|             mockConditionManager.metadataLoadCompleted.andReturn(false); | ||||
|             mockConditionManager.getEvaluator.andReturn(mockEvaluator); | ||||
|             mockConditionManager.getComposition.andReturn({}); | ||||
|             mockConditionManager.getTelemetryMetadata.andReturn({}); | ||||
|             mockConditionManager.getObjectName.andReturn('Object Name'); | ||||
|             mockConditionManager.getTelemetryPropertyName.andReturn('Property Name'); | ||||
|  | ||||
|             mockWidgetDnD = jasmine.createSpyObj('dnd', [ | ||||
|                 'on', | ||||
|                 'setDragImage', | ||||
|                 'dragStart' | ||||
|             ]); | ||||
|  | ||||
|             mockContainer = $(document.createElement('div')); | ||||
|  | ||||
|             removeSpy = jasmine.createSpy('removeCallback'); | ||||
|             duplicateSpy = jasmine.createSpy('duplicateCallback'); | ||||
|             changeSpy = jasmine.createSpy('changeCallback'); | ||||
|             conditionChangeSpy = jasmine.createSpy('conditionChangeCallback'); | ||||
|  | ||||
|             testRule = new Rule(mockRuleConfig, mockDomainObject, mockOpenMCT, mockConditionManager, | ||||
|                                 mockWidgetDnD); | ||||
|             testRule.on('remove', removeSpy); | ||||
|             testRule.on('duplicate', duplicateSpy); | ||||
|             testRule.on('change', changeSpy); | ||||
|             testRule.on('conditionChange', conditionChangeSpy); | ||||
|         }); | ||||
|  | ||||
|         it('closes its configuration panel on initial load', function () { | ||||
|             expect(testRule.getProperty('expanded')).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('gets its DOM element', function () { | ||||
|             mockContainer.append(testRule.getDOM()); | ||||
|             expect($('.l-widget-rule', mockContainer).get().length).toBeGreaterThan(0); | ||||
|         }); | ||||
|  | ||||
|         it('gets its configuration properties', function () { | ||||
|             expect(testRule.getProperty('name')).toEqual('Name'); | ||||
|             expect(testRule.getProperty('icon')).toEqual('test-icon-name'); | ||||
|         }); | ||||
|  | ||||
|         it('can duplicate itself', function () { | ||||
|             testRule.duplicate(); | ||||
|             mockRuleConfig.expanded = true; | ||||
|             expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig); | ||||
|         }); | ||||
|  | ||||
|         it('can remove itself from the configuration', function () { | ||||
|             testRule.remove(); | ||||
|             expect(removeSpy).toHaveBeenCalled(); | ||||
|             expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined(); | ||||
|             expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']); | ||||
|         }); | ||||
|  | ||||
|         it('updates its configuration on a condition change and invokes callbacks', function () { | ||||
|             testRule.onConditionChange({ | ||||
|                 value: 'newValue', | ||||
|                 property: 'object', | ||||
|                 index: 0 | ||||
|             }); | ||||
|             expect(testRule.getProperty('conditions')[0].object).toEqual('newValue'); | ||||
|             expect(conditionChangeSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('allows initializing a new condition with a default configuration', function () { | ||||
|             testRule.initCondition(); | ||||
|             expect(mockRuleConfig.conditions).toEqual([{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'blah', | ||||
|                 key: 'blah', | ||||
|                 operation: 'blah', | ||||
|                 values: ['blah.', 'blah!', 'blah?'] | ||||
|             },{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             }]); | ||||
|         }); | ||||
|  | ||||
|         it('allows initializing a new condition from a given configuration', function () { | ||||
|             testRule.initCondition({ | ||||
|                 sourceCondition: { | ||||
|                     object: 'object1', | ||||
|                     key: 'key1', | ||||
|                     operation: 'operation1', | ||||
|                     values: [1, 2, 3] | ||||
|                 }, | ||||
|                 index: 0 | ||||
|             }); | ||||
|             expect(mockRuleConfig.conditions).toEqual([{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 operation: '', | ||||
|                 values: [] | ||||
|             },{ | ||||
|                 object: 'object1', | ||||
|                 key: 'key1', | ||||
|                 operation: 'operation1', | ||||
|                 values: [1, 2, 3] | ||||
|             },{ | ||||
|                 object: 'blah', | ||||
|                 key: 'blah', | ||||
|                 operation: 'blah', | ||||
|                 values: ['blah.', 'blah!', 'blah?'] | ||||
|             }]); | ||||
|         }); | ||||
|  | ||||
|         it('invokes mutate when updating the domain object', function () { | ||||
|             testRule.updateDomainObject(); | ||||
|             expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('builds condition view from condition configuration', function () { | ||||
|             mockContainer.append(testRule.getDOM()); | ||||
|             expect($('.t-condition', mockContainer).get().length).toEqual(2); | ||||
|         }); | ||||
|  | ||||
|         it('responds to input of style properties, and updates the preview', function () { | ||||
|             testRule.colorInputs['background-color'].set('#434343'); | ||||
|             expect(mockRuleConfig.style['background-color']).toEqual('#434343'); | ||||
|             testRule.colorInputs['border-color'].set('#666666'); | ||||
|             expect(mockRuleConfig.style['border-color']).toEqual('#666666'); | ||||
|             testRule.colorInputs.color.set('#999999'); | ||||
|             expect(mockRuleConfig.style.color).toEqual('#999999'); | ||||
|  | ||||
|             expect(testRule.thumbnail.css('background-color')).toEqual('rgb(67, 67, 67)'); | ||||
|             expect(testRule.thumbnail.css('border-color')).toEqual('rgb(102, 102, 102)'); | ||||
|             expect(testRule.thumbnail.css('color')).toEqual('rgb(153, 153, 153)'); | ||||
|  | ||||
|             expect(changeSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('responds to input for the icon property', function () { | ||||
|             testRule.iconInput.set('icon-alert-rect'); | ||||
|             expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); | ||||
|             expect(changeSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         /* | ||||
|         test for js condition commented out for v1 | ||||
|         */ | ||||
|  | ||||
|         // it('responds to input of text properties', function () { | ||||
|         //     var testInputs = ['name', 'label', 'message', 'jsCondition'], | ||||
|         //         input; | ||||
|  | ||||
|         //     testInputs.forEach(function (key) { | ||||
|         //         input = testRule.textInputs[key]; | ||||
|         //         input.prop('value', 'A new ' + key); | ||||
|         //         input.trigger('input'); | ||||
|         //         expect(mockRuleConfig[key]).toEqual('A new ' + key); | ||||
|         //     }); | ||||
|  | ||||
|         //     expect(changeSpy).toHaveBeenCalled(); | ||||
|         // }); | ||||
|  | ||||
|         it('allows input for when the rule triggers', function () { | ||||
|             testRule.trigger.prop('value', 'all'); | ||||
|             testRule.trigger.trigger('change'); | ||||
|             expect(testRule.config.trigger).toEqual('all'); | ||||
|             expect(conditionChangeSpy).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('generates a human-readable description from its conditions', function () { | ||||
|             testRule.generateDescription(); | ||||
|             expect(testRule.config.description).toContain( | ||||
|                 'Object Name\'s Property Name Operation Description' | ||||
|             ); | ||||
|             testRule.config.trigger = 'js'; | ||||
|             testRule.generateDescription(); | ||||
|             expect(testRule.config.description).toContain( | ||||
|                 'when a custom JavaScript condition evaluates to true' | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|         it('initiates a drag event when its grippy is clicked', function () { | ||||
|             testRule.grippy.trigger('mousedown'); | ||||
|             expect(mockWidgetDnD.setDragImage).toHaveBeenCalled(); | ||||
|             expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule'); | ||||
|         }); | ||||
|  | ||||
|         /* | ||||
|         test for js condition commented out for v1 | ||||
|         */ | ||||
|  | ||||
|         it('can remove a condition from its configuration', function () { | ||||
|             testRule.removeCondition(0); | ||||
|             expect(testRule.config.conditions).toEqual([{ | ||||
|                 object: 'blah', | ||||
|                 key: 'blah', | ||||
|                 operation: 'blah', | ||||
|                 values: ['blah.', 'blah!', 'blah?'] | ||||
|             }]); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										165
									
								
								src/plugins/summaryWidget/test/SummaryWidgetSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/plugins/summaryWidget/test/SummaryWidgetSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) { | ||||
|     describe('The Summary Widget', function () { | ||||
|         var summaryWidget, | ||||
|             mockDomainObject, | ||||
|             mockOldDomainObject, | ||||
|             mockOpenMCT, | ||||
|             mockObjectService, | ||||
|             mockStatusCapability, | ||||
|             mockComposition, | ||||
|             mockContainer, | ||||
|             listenCallback, | ||||
|             listenCallbackSpy; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'testKey' | ||||
|                 }, | ||||
|                 name: 'testName', | ||||
|                 composition: [], | ||||
|                 configuration: {} | ||||
|             }; | ||||
|             mockComposition = jasmine.createSpyObj('composition', [ | ||||
|                 'on', | ||||
|                 'off', | ||||
|                 'load' | ||||
|             ]); | ||||
|             mockStatusCapability = jasmine.createSpyObj('statusCapability', [ | ||||
|                 'get', | ||||
|                 'listen', | ||||
|                 'triggerCallback' | ||||
|             ]); | ||||
|  | ||||
|             listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); | ||||
|             mockStatusCapability.get.andReturn([]); | ||||
|             mockStatusCapability.listen.andCallFake(function (callback) { | ||||
|                 listenCallback = callback; | ||||
|                 return listenCallbackSpy; | ||||
|             }); | ||||
|             mockStatusCapability.triggerCallback.andCallFake(function () { | ||||
|                 listenCallback(['editing']); | ||||
|             }); | ||||
|  | ||||
|             mockOldDomainObject = {}; | ||||
|             mockOldDomainObject.getCapability = jasmine.createSpy('capability'); | ||||
|             mockOldDomainObject.getCapability.andReturn(mockStatusCapability); | ||||
|  | ||||
|             mockObjectService = {}; | ||||
|             mockObjectService.getObjects = jasmine.createSpy('objectService'); | ||||
|             mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) { | ||||
|                 resolve({ | ||||
|                     testKey: mockOldDomainObject | ||||
|                 }); | ||||
|             })); | ||||
|             mockOpenMCT = jasmine.createSpyObj('openmct', [ | ||||
|                 '$injector', | ||||
|                 'composition', | ||||
|                 'objects' | ||||
|             ]); | ||||
|             mockOpenMCT.$injector.get = jasmine.createSpy('get'); | ||||
|             mockOpenMCT.$injector.get.andReturn(mockObjectService); | ||||
|             mockOpenMCT.composition = jasmine.createSpyObj('composition', [ | ||||
|                 'get', | ||||
|                 'on' | ||||
|             ]); | ||||
|             mockOpenMCT.composition.get.andReturn(mockComposition); | ||||
|             mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); | ||||
|             mockOpenMCT.objects.observe = function () {}; | ||||
|  | ||||
|             summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT); | ||||
|             mockContainer = document.createElement('div'); | ||||
|             summaryWidget.show(mockContainer); | ||||
|         }); | ||||
|  | ||||
|         it('adds its DOM element to the view', function () { | ||||
|             expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); | ||||
|         }); | ||||
|  | ||||
|         it('initialzes a default rule', function () { | ||||
|             expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); | ||||
|             expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); | ||||
|         }); | ||||
|  | ||||
|         it('builds rules and rule placeholders in view from configuration', function () { | ||||
|             expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(2); | ||||
|         }); | ||||
|  | ||||
|         it('allows initializing a new rule with a particular identifier', function () { | ||||
|             summaryWidget.initRule('rule0', 'Rule'); | ||||
|             expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('allows adding a new rule with a unique identifier to the configuration and view', function () { | ||||
|             summaryWidget.addRule(); | ||||
|             expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2); | ||||
|             mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { | ||||
|                 expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); | ||||
|             }); | ||||
|             summaryWidget.addRule(); | ||||
|             expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3); | ||||
|             mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { | ||||
|                 expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); | ||||
|             }); | ||||
|             expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(6); | ||||
|         }); | ||||
|  | ||||
|         it('allows duplicating a rule from source configuration', function () { | ||||
|             var sourceConfig = JSON.parse(JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)); | ||||
|             summaryWidget.duplicateRule(sourceConfig); | ||||
|             expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2); | ||||
|         }); | ||||
|  | ||||
|         it('does not duplicate an existing rule in the configuration', function () { | ||||
|             summaryWidget.initRule('default', 'Default'); | ||||
|             expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1); | ||||
|         }); | ||||
|  | ||||
|         it('uses mutate when updating the domain object', function () { | ||||
|             summaryWidget.updateDomainObject(); | ||||
|             expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { | ||||
|             setTimeout(function () { | ||||
|                 summaryWidget.onEdit([]); | ||||
|                 expect(summaryWidget.editing).toEqual(false); | ||||
|                 expect(summaryWidget.ruleArea.css('display')).toEqual('none'); | ||||
|                 expect(summaryWidget.testDataArea.css('display')).toEqual('none'); | ||||
|                 expect(summaryWidget.addRuleButton.css('display')).toEqual('none'); | ||||
|                 summaryWidget.onEdit(['editing']); | ||||
|                 expect(summaryWidget.editing).toEqual(true); | ||||
|                 expect(summaryWidget.ruleArea.css('display')).not.toEqual('none'); | ||||
|                 expect(summaryWidget.testDataArea.css('display')).not.toEqual('none'); | ||||
|                 expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none'); | ||||
|             }, 100); | ||||
|         }); | ||||
|  | ||||
|         it('unregisters any registered listeners on a destroy', function () { | ||||
|             setTimeout(function () { | ||||
|                 summaryWidget.destroy(); | ||||
|                 expect(listenCallbackSpy).toHaveBeenCalled(); | ||||
|             }, 100); | ||||
|         }); | ||||
|  | ||||
|         it('allows reorders of rules', function () { | ||||
|             summaryWidget.initRule('rule0'); | ||||
|             summaryWidget.initRule('rule1'); | ||||
|             summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1']; | ||||
|             summaryWidget.reorder({ | ||||
|                 draggingId: 'rule1', | ||||
|                 dropTarget: 'default' | ||||
|             }); | ||||
|             expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual(['default', 'rule1', 'rule0']); | ||||
|         }); | ||||
|  | ||||
|         it('adds hyperlink to the widget button and sets newTab preference', function () { | ||||
|             summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab'); | ||||
|  | ||||
|             var widgetButton = $('#widget', mockContainer); | ||||
|  | ||||
|             expect(widgetButton.attr('href')).toEqual('https://www.nasa.gov'); | ||||
|             expect(widgetButton.attr('target')).toEqual('_blank'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										140
									
								
								src/plugins/summaryWidget/test/TestDataItemSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/plugins/summaryWidget/test/TestDataItemSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) { | ||||
|     describe('A summary widget test data item', function () { | ||||
|         var testDataItem, | ||||
|             mockConfig, | ||||
|             mockConditionManager, | ||||
|             mockContainer, | ||||
|             mockEvaluator, | ||||
|             changeSpy, | ||||
|             duplicateSpy, | ||||
|             removeSpy, | ||||
|             generateValueSpy; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockContainer = $(document.createElement('div')); | ||||
|  | ||||
|             mockConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'property1', | ||||
|                 value: 1 | ||||
|             }; | ||||
|  | ||||
|             mockEvaluator = {}; | ||||
|             mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); | ||||
|  | ||||
|             mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ | ||||
|                 'on', | ||||
|                 'getComposition', | ||||
|                 'loadCompleted', | ||||
|                 'getEvaluator', | ||||
|                 'getTelemetryMetadata', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'getObjectName', | ||||
|                 'getTelemetryPropertyName', | ||||
|                 'getTelemetryPropertyType' | ||||
|             ]); | ||||
|             mockConditionManager.loadCompleted.andReturn(false); | ||||
|             mockConditionManager.metadataLoadCompleted.andReturn(false); | ||||
|             mockConditionManager.getEvaluator.andReturn(mockEvaluator); | ||||
|             mockConditionManager.getComposition.andReturn({}); | ||||
|             mockConditionManager.getTelemetryMetadata.andReturn({}); | ||||
|             mockConditionManager.getObjectName.andReturn('Object Name'); | ||||
|             mockConditionManager.getTelemetryPropertyName.andReturn('Property Name'); | ||||
|             mockConditionManager.getTelemetryPropertyType.andReturn(''); | ||||
|  | ||||
|             duplicateSpy = jasmine.createSpy('duplicate'); | ||||
|             removeSpy = jasmine.createSpy('remove'); | ||||
|             changeSpy = jasmine.createSpy('change'); | ||||
|             generateValueSpy = jasmine.createSpy('generateValueInput'); | ||||
|  | ||||
|             testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); | ||||
|  | ||||
|             testDataItem.on('duplicate', duplicateSpy); | ||||
|             testDataItem.on('remove', removeSpy); | ||||
|             testDataItem.on('change', changeSpy); | ||||
|         }); | ||||
|  | ||||
|         it('exposes a DOM element to represent itself in the view', function () { | ||||
|             mockContainer.append(testDataItem.getDOM()); | ||||
|             expect($('.t-test-data-item', mockContainer).get().length).toEqual(1); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its object select', function () { | ||||
|             testDataItem.selects.object.setSelected(''); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: '', | ||||
|                 property: 'object', | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its key select', function () { | ||||
|             testDataItem.generateValueInput = generateValueSpy; | ||||
|             testDataItem.selects.key.setSelected(''); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: '', | ||||
|                 property: 'key', | ||||
|                 index: 54 | ||||
|             }); | ||||
|             expect(generateValueSpy).toHaveBeenCalledWith(''); | ||||
|         }); | ||||
|  | ||||
|         it('generates a value input of the appropriate type', function () { | ||||
|             mockContainer.append(testDataItem.getDOM()); | ||||
|             mockEvaluator.getInputTypeById.andReturn('number'); | ||||
|             testDataItem.generateValueInput(''); | ||||
|             expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1); | ||||
|             expect($('input', mockContainer).prop('valueAsNumber')).toEqual(1); | ||||
|  | ||||
|             mockEvaluator.getInputTypeById.andReturn('text'); | ||||
|             testDataItem.config.value = 'Text I Am'; | ||||
|             testDataItem.generateValueInput(''); | ||||
|             expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1); | ||||
|             expect($('input', mockContainer).prop('value')).toEqual('Text I Am'); | ||||
|         }); | ||||
|  | ||||
|         it('ensures reasonable defaults on values if none are provided', function () { | ||||
|             mockContainer.append(testDataItem.getDOM()); | ||||
|  | ||||
|             mockEvaluator.getInputTypeById.andReturn('number'); | ||||
|             testDataItem.config.value = undefined; | ||||
|             testDataItem.generateValueInput(''); | ||||
|             expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1); | ||||
|             expect($('input', mockContainer).prop('valueAsNumber')).toEqual(0); | ||||
|             expect(testDataItem.config.value).toEqual(0); | ||||
|  | ||||
|             mockEvaluator.getInputTypeById.andReturn('text'); | ||||
|             testDataItem.config.value = undefined; | ||||
|             testDataItem.generateValueInput(''); | ||||
|             expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1); | ||||
|             expect($('input', mockContainer).prop('value')).toEqual(''); | ||||
|             expect(testDataItem.config.value).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('responds to a change in its value inputs', function () { | ||||
|             mockContainer.append(testDataItem.getDOM()); | ||||
|             mockEvaluator.getInputTypeById.andReturn('number'); | ||||
|             testDataItem.generateValueInput(''); | ||||
|             $('input', mockContainer).prop('value', 9001); | ||||
|             $('input', mockContainer).trigger('input'); | ||||
|             expect(changeSpy).toHaveBeenCalledWith({ | ||||
|                 value: 9001, | ||||
|                 property: 'value', | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('can remove itself from the configuration', function () { | ||||
|             testDataItem.remove(); | ||||
|             expect(removeSpy).toHaveBeenCalledWith(54); | ||||
|         }); | ||||
|  | ||||
|         it('can duplicate itself', function () { | ||||
|             testDataItem.duplicate(); | ||||
|             expect(duplicateSpy).toHaveBeenCalledWith({ | ||||
|                 sourceItem: mockConfig, | ||||
|                 index: 54 | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										231
									
								
								src/plugins/summaryWidget/test/TestDataManagerSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/plugins/summaryWidget/test/TestDataManagerSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) { | ||||
|     describe('A Summary Widget Rule', function () { | ||||
|         var mockDomainObject, | ||||
|             mockOpenMCT, | ||||
|             mockConditionManager, | ||||
|             mockEvaluator, | ||||
|             mockContainer, | ||||
|             mockTelemetryMetadata, | ||||
|             testDataManager, | ||||
|             mockCompObject1, | ||||
|             mockCompObject2; | ||||
|  | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockDomainObject = { | ||||
|                 configuration: { | ||||
|                     testDataConfig: [{ | ||||
|                         object: '', | ||||
|                         key: '', | ||||
|                         value: '' | ||||
|                     },{ | ||||
|                         object: 'object1', | ||||
|                         key: 'property1', | ||||
|                         value: 66 | ||||
|                     },{ | ||||
|                         object: 'object2', | ||||
|                         key: 'property4', | ||||
|                         value: 'Text It Is' | ||||
|                     }] | ||||
|                 }, | ||||
|                 composition: [{ | ||||
|                     object1: { | ||||
|                         key: 'object1', | ||||
|                         name: 'Object 1' | ||||
|                     }, | ||||
|                     object2: { | ||||
|                         key: 'object2', | ||||
|                         name: 'Object 2' | ||||
|                     } | ||||
|                 }] | ||||
|             }; | ||||
|  | ||||
|             mockTelemetryMetadata = { | ||||
|                 object1: { | ||||
|                     property1: { | ||||
|                         key: 'property1' | ||||
|                     }, | ||||
|                     property2: { | ||||
|                         key: 'property2' | ||||
|                     } | ||||
|                 }, | ||||
|                 object2 : { | ||||
|                     property3: { | ||||
|                         key: 'property3' | ||||
|                     }, | ||||
|                     property4: { | ||||
|                         key: 'property4' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockCompObject1 = { | ||||
|                 identifier: { | ||||
|                     key: 'object1' | ||||
|                 }, | ||||
|                 name: 'Object 1' | ||||
|             }; | ||||
|             mockCompObject2 = { | ||||
|                 identifier: { | ||||
|                     key: 'object2' | ||||
|                 }, | ||||
|                 name: 'Object 2' | ||||
|             }; | ||||
|  | ||||
|             mockOpenMCT = {}; | ||||
|             mockOpenMCT.objects = {}; | ||||
|             mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); | ||||
|  | ||||
|             mockEvaluator = {}; | ||||
|             mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache'); | ||||
|             mockEvaluator.useTestData = jasmine.createSpy('useTestData'); | ||||
|  | ||||
|             mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ | ||||
|                 'on', | ||||
|                 'getComposition', | ||||
|                 'loadCompleted', | ||||
|                 'getEvaluator', | ||||
|                 'getTelemetryMetadata', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'getObjectName', | ||||
|                 'getTelemetryPropertyName', | ||||
|                 'triggerTelemetryCallback' | ||||
|             ]); | ||||
|             mockConditionManager.loadCompleted.andReturn(false); | ||||
|             mockConditionManager.metadataLoadCompleted.andReturn(false); | ||||
|             mockConditionManager.getEvaluator.andReturn(mockEvaluator); | ||||
|             mockConditionManager.getComposition.andReturn({ | ||||
|                 object1: mockCompObject1, | ||||
|                 object2: mockCompObject2 | ||||
|             }); | ||||
|             mockConditionManager.getTelemetryMetadata.andCallFake(function (id) { | ||||
|                 return mockTelemetryMetadata[id]; | ||||
|             }); | ||||
|             mockConditionManager.getObjectName.andReturn('Object Name'); | ||||
|             mockConditionManager.getTelemetryPropertyName.andReturn('Property Name'); | ||||
|  | ||||
|             mockContainer = $(document.createElement('div')); | ||||
|  | ||||
|             testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT); | ||||
|         }); | ||||
|  | ||||
|         it('closes its configuration panel on initial load', function () { | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it('exposes a DOM element to represent itself in the view', function () { | ||||
|             mockContainer.append(testDataManager.getDOM()); | ||||
|             expect($('.t-widget-test-data-content', mockContainer).get().length).toBeGreaterThan(0); | ||||
|         }); | ||||
|  | ||||
|         it('generates a test cache in the format expected by a condition evaluator', function () { | ||||
|             testDataManager.updateTestCache(); | ||||
|             expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ | ||||
|                 object1: { | ||||
|                     property1: 66, | ||||
|                     property2: '' | ||||
|                 }, | ||||
|                 object2: { | ||||
|                     property3: '', | ||||
|                     property4: 'Text It Is' | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('updates its configuration on a item change and provides an updated' + | ||||
|            'cache to the evaluator', function () { | ||||
|             testDataManager.onItemChange({ | ||||
|                 value: 26, | ||||
|                 property: 'value', | ||||
|                 index: 1 | ||||
|             }); | ||||
|             expect(testDataManager.config[1].value).toEqual(26); | ||||
|             expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ | ||||
|                 object1: { | ||||
|                     property1: 26, | ||||
|                     property2: '' | ||||
|                 }, | ||||
|                 object2: { | ||||
|                     property3: '', | ||||
|                     property4: 'Text It Is' | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('allows initializing a new item with a default configuration', function () { | ||||
|             testDataManager.initItem(); | ||||
|             expect(mockDomainObject.configuration.testDataConfig).toEqual([{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 value: '' | ||||
|             },{ | ||||
|                 object: 'object1', | ||||
|                 key: 'property1', | ||||
|                 value: 66 | ||||
|             },{ | ||||
|                 object: 'object2', | ||||
|                 key: 'property4', | ||||
|                 value: 'Text It Is' | ||||
|             },{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 value: '' | ||||
|             }]); | ||||
|         }); | ||||
|  | ||||
|         it('allows initializing a new item from a given configuration', function () { | ||||
|             testDataManager.initItem({ | ||||
|                 sourceItem: { | ||||
|                     object: 'object2', | ||||
|                     key: 'property3', | ||||
|                     value: 1 | ||||
|                 }, | ||||
|                 index: 0 | ||||
|             }); | ||||
|             expect(mockDomainObject.configuration.testDataConfig).toEqual([{ | ||||
|                 object: '', | ||||
|                 key: '', | ||||
|                 value: '' | ||||
|             },{ | ||||
|                 object: 'object2', | ||||
|                 key: 'property3', | ||||
|                 value: 1 | ||||
|             },{ | ||||
|                 object: 'object1', | ||||
|                 key: 'property1', | ||||
|                 value: 66 | ||||
|             },{ | ||||
|                 object: 'object2', | ||||
|                 key: 'property4', | ||||
|                 value: 'Text It Is' | ||||
|             }]); | ||||
|         }); | ||||
|  | ||||
|         it('invokes mutate when updating the domain object', function () { | ||||
|             testDataManager.updateDomainObject(); | ||||
|             expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it('builds item view from item configuration', function () { | ||||
|             mockContainer.append(testDataManager.getDOM()); | ||||
|             expect($('.t-test-data-item', mockContainer).get().length).toEqual(3); | ||||
|         }); | ||||
|  | ||||
|         it('can remove a item from its configuration', function () { | ||||
|             testDataManager.removeItem(0); | ||||
|             expect(mockDomainObject.configuration.testDataConfig).toEqual([{ | ||||
|                 object: 'object1', | ||||
|                 key: 'property1', | ||||
|                 value: 66 | ||||
|             },{ | ||||
|                 object: 'object2', | ||||
|                 key: 'property4', | ||||
|                 value: 'Text It Is' | ||||
|             }]); | ||||
|         }); | ||||
|  | ||||
|         it('exposes a UI element to toggle test data on and off', function () { | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										0
									
								
								src/plugins/summaryWidget/test/WidgetDnDSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/plugins/summaryWidget/test/WidgetDnDSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										23
									
								
								src/plugins/summaryWidget/test/input/ColorPaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/summaryWidget/test/input/ColorPaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| define(['../../src/input/ColorPalette'], function (ColorPalette) { | ||||
|     describe('An Open MCT color palette', function () { | ||||
|         var colorPalette, changeCallback; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             changeCallback = jasmine.createSpy('changeCallback'); | ||||
|         }); | ||||
|  | ||||
|         it('allows defining a custom color set', function () { | ||||
|             colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); | ||||
|             expect(colorPalette.getCurrent()).toEqual('color1'); | ||||
|             colorPalette.on('change', changeCallback); | ||||
|             colorPalette.set('color2'); | ||||
|             expect(colorPalette.getCurrent()).toEqual('color2'); | ||||
|             expect(changeCallback).toHaveBeenCalledWith('color2'); | ||||
|         }); | ||||
|  | ||||
|         it('loads with a default color set if one is not provided', function () { | ||||
|             colorPalette = new ColorPalette('someClass', 'someContainer'); | ||||
|             expect(colorPalette.getCurrent()).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										23
									
								
								src/plugins/summaryWidget/test/input/IconPaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/summaryWidget/test/input/IconPaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| define(['../../src/input/IconPalette'], function (IconPalette) { | ||||
|     describe('An Open MCT icon palette', function () { | ||||
|         var iconPalette, changeCallback; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             changeCallback = jasmine.createSpy('changeCallback'); | ||||
|         }); | ||||
|  | ||||
|         it('allows defining a custom icon set', function () { | ||||
|             iconPalette = new IconPalette('','someContainer', ['icon1', 'icon2', 'icon3']); | ||||
|             expect(iconPalette.getCurrent()).toEqual('icon1'); | ||||
|             iconPalette.on('change', changeCallback); | ||||
|             iconPalette.set('icon2'); | ||||
|             expect(iconPalette.getCurrent()).toEqual('icon2'); | ||||
|             expect(changeCallback).toHaveBeenCalledWith('icon2'); | ||||
|         }); | ||||
|  | ||||
|         it('loads with a default icon set if one is not provided', function () { | ||||
|             iconPalette = new IconPalette('someClass', 'someContainer'); | ||||
|             expect(iconPalette.getCurrent()).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										122
									
								
								src/plugins/summaryWidget/test/input/KeySelectSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/plugins/summaryWidget/test/input/KeySelectSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| define(['../../src/input/KeySelect'], function (KeySelect) { | ||||
|     describe('A select for choosing composition object properties', function () { | ||||
|         var mockConfig, mockBadConfig, mockManager, keySelect, mockMetadata, mockObjectSelect; | ||||
|         beforeEach(function () { | ||||
|             mockConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'a' | ||||
|             }; | ||||
|  | ||||
|             mockBadConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'someNonexistentKey' | ||||
|             }; | ||||
|  | ||||
|             mockMetadata = { | ||||
|                 object1: { | ||||
|                     a: { | ||||
|                         name: 'A' | ||||
|                     }, | ||||
|                     b: { | ||||
|                         name: 'B' | ||||
|                     } | ||||
|                 }, | ||||
|                 object2: { | ||||
|                     alpha: { | ||||
|                         name: 'Alpha' | ||||
|                     }, | ||||
|                     beta: { | ||||
|                         name: 'Beta' | ||||
|                     } | ||||
|                 }, | ||||
|                 object3: { | ||||
|                     a: { | ||||
|                         name: 'A' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockManager = jasmine.createSpyObj('mockManager', [ | ||||
|                 'on', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'triggerCallback', | ||||
|                 'getTelemetryMetadata' | ||||
|             ]); | ||||
|  | ||||
|             mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', [ | ||||
|                 'on', | ||||
|                 'triggerCallback' | ||||
|             ]); | ||||
|  | ||||
|             mockObjectSelect.on.andCallFake(function (event, callback) { | ||||
|                 this.callbacks = this.callbacks || {}; | ||||
|                 this.callbacks[event] = callback; | ||||
|             }); | ||||
|  | ||||
|             mockObjectSelect.triggerCallback.andCallFake(function (event, key) { | ||||
|                 this.callbacks[event](key); | ||||
|             }); | ||||
|  | ||||
|             mockManager.on.andCallFake(function (event, callback) { | ||||
|                 this.callbacks = this.callbacks || {}; | ||||
|                 this.callbacks[event] = callback; | ||||
|             }); | ||||
|  | ||||
|             mockManager.triggerCallback.andCallFake(function (event) { | ||||
|                 this.callbacks[event](); | ||||
|             }); | ||||
|  | ||||
|             mockManager.getTelemetryMetadata.andCallFake(function (key) { | ||||
|                 return mockMetadata[key]; | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it('waits until the metadata fully loads to populate itself', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(false); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             expect(keySelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with metadata on a metadata load', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(false); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             mockManager.triggerCallback('metadata'); | ||||
|             expect(keySelect.getSelected()).toEqual('a'); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with metadata if metadata load is already complete', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             expect(keySelect.getSelected()).toEqual('a'); | ||||
|         }); | ||||
|  | ||||
|         it('clears its selection state if the property in its config is not in its object', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager); | ||||
|             expect(keySelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('populates with the appropriate options when its linked object changes', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             mockObjectSelect.triggerCallback('change', 'object2'); | ||||
|             keySelect.setSelected('alpha'); | ||||
|             expect(keySelect.getSelected()).toEqual('alpha'); | ||||
|         }); | ||||
|  | ||||
|         it('clears its selected state on change if the field is not present in the new object', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             mockObjectSelect.triggerCallback('change', 'object2'); | ||||
|             expect(keySelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('maintains its selected state on change if field is present in new object', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); | ||||
|             mockObjectSelect.triggerCallback('change', 'object3'); | ||||
|             expect(keySelect.getSelected()).toEqual('a'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										109
									
								
								src/plugins/summaryWidget/test/input/ObjectSelectSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/plugins/summaryWidget/test/input/ObjectSelectSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| define(['../../src/input/ObjectSelect'], function (ObjectSelect) { | ||||
|     describe('A select for choosing composition objects', function () { | ||||
|         var mockConfig, mockBadConfig, mockManager, objectSelect, mockComposition; | ||||
|         beforeEach(function () { | ||||
|             mockConfig = { | ||||
|                 object: 'key1' | ||||
|             }; | ||||
|  | ||||
|             mockBadConfig = { | ||||
|                 object: 'someNonexistentObject' | ||||
|             }; | ||||
|  | ||||
|             mockComposition = { | ||||
|                 key1: { | ||||
|                     identifier: { | ||||
|                         key: 'key1' | ||||
|                     }, | ||||
|                     name: 'Object 1' | ||||
|                 }, | ||||
|                 key2: { | ||||
|                     identifier: { | ||||
|                         key: 'key2' | ||||
|                     }, | ||||
|                     name: 'Object 2' | ||||
|                 } | ||||
|             }; | ||||
|             mockManager = jasmine.createSpyObj('mockManager', [ | ||||
|                 'on', | ||||
|                 'loadCompleted', | ||||
|                 'triggerCallback', | ||||
|                 'getComposition' | ||||
|             ]); | ||||
|  | ||||
|             mockManager.on.andCallFake(function (event, callback) { | ||||
|                 this.callbacks = this.callbacks || {}; | ||||
|                 this.callbacks[event] = callback; | ||||
|             }); | ||||
|  | ||||
|             mockManager.triggerCallback.andCallFake(function (event, newObj) { | ||||
|                 if (event === 'add') { | ||||
|                     this.callbacks.add(newObj); | ||||
|                 } else { | ||||
|                     this.callbacks[event](); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             mockManager.getComposition.andCallFake(function () { | ||||
|                 return mockComposition; | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it('allows setting special keyword options', function () { | ||||
|             mockManager.loadCompleted.andReturn(true); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager, [ | ||||
|                 ['keyword1', 'A special option'], | ||||
|                 ['keyword2', 'A special option'] | ||||
|             ]); | ||||
|             objectSelect.setSelected('keyword1'); | ||||
|             expect(objectSelect.getSelected()).toEqual('keyword1'); | ||||
|         }); | ||||
|  | ||||
|         it('waits until the composition fully loads to populate itself', function () { | ||||
|             mockManager.loadCompleted.andReturn(false); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager); | ||||
|             expect(objectSelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with composition objects on a composition load', function () { | ||||
|             mockManager.loadCompleted.andReturn(false); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager); | ||||
|             mockManager.triggerCallback('load'); | ||||
|             expect(objectSelect.getSelected()).toEqual('key1'); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with composition objects if load is already complete', function () { | ||||
|             mockManager.loadCompleted.andReturn(true); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager); | ||||
|             expect(objectSelect.getSelected()).toEqual('key1'); | ||||
|         }); | ||||
|  | ||||
|         it('clears its selection state if the object in its config is not in the composition', function () { | ||||
|             mockManager.loadCompleted.andReturn(true); | ||||
|             objectSelect = new ObjectSelect(mockBadConfig, mockManager); | ||||
|             expect(objectSelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('adds a new option on a composition add', function () { | ||||
|             mockManager.loadCompleted.andReturn(true); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager); | ||||
|             mockManager.triggerCallback('add', { | ||||
|                 identifier: { | ||||
|                     key: 'key3' | ||||
|                 }, | ||||
|                 name: 'Object 3' | ||||
|             }); | ||||
|             objectSelect.setSelected('key3'); | ||||
|             expect(objectSelect.getSelected()).toEqual('key3'); | ||||
|         }); | ||||
|  | ||||
|         it('removes an option on a composition remove', function () { | ||||
|             mockManager.loadCompleted.andReturn(true); | ||||
|             objectSelect = new ObjectSelect(mockConfig, mockManager); | ||||
|             delete mockComposition.key1; | ||||
|             mockManager.triggerCallback('remove'); | ||||
|             expect(objectSelect.getSelected()).not.toEqual('key1'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										142
									
								
								src/plugins/summaryWidget/test/input/OperationSelectSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/plugins/summaryWidget/test/input/OperationSelectSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| define(['../../src/input/OperationSelect'], function (OperationSelect) { | ||||
|     describe('A select for choosing composition object properties', function () { | ||||
|         var mockConfig, mockBadConfig, mockManager, operationSelect, mockOperations, | ||||
|             mockPropertyTypes, mockKeySelect, mockEvaluator; | ||||
|         beforeEach(function () { | ||||
|  | ||||
|             mockConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'a', | ||||
|                 operation: 'operation1' | ||||
|             }; | ||||
|  | ||||
|             mockBadConfig = { | ||||
|                 object: 'object1', | ||||
|                 key: 'a', | ||||
|                 operation: 'someNonexistentOperation' | ||||
|             }; | ||||
|  | ||||
|             mockOperations = { | ||||
|                 operation1: { | ||||
|                     text: 'An operation', | ||||
|                     appliesTo: ['number'] | ||||
|                 }, | ||||
|                 operation2: { | ||||
|                     text: 'Another operation', | ||||
|                     appliesTo: ['string'] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockPropertyTypes = { | ||||
|                 object1: { | ||||
|                     a: 'number', | ||||
|                     b: 'string', | ||||
|                     c: 'number' | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockManager = jasmine.createSpyObj('mockManager', [ | ||||
|                 'on', | ||||
|                 'metadataLoadCompleted', | ||||
|                 'triggerCallback', | ||||
|                 'getTelemetryPropertyType', | ||||
|                 'getEvaluator' | ||||
|  | ||||
|             ]); | ||||
|  | ||||
|             mockKeySelect = jasmine.createSpyObj('mockKeySelect', [ | ||||
|                 'on', | ||||
|                 'triggerCallback' | ||||
|             ]); | ||||
|  | ||||
|             mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ | ||||
|                 'getOperationKeys', | ||||
|                 'operationAppliesTo', | ||||
|                 'getOperationText' | ||||
|             ]); | ||||
|  | ||||
|             mockEvaluator.getOperationKeys.andReturn(Object.keys(mockOperations)); | ||||
|  | ||||
|             mockEvaluator.getOperationText.andCallFake(function (key) { | ||||
|                 return mockOperations[key].text; | ||||
|             }); | ||||
|  | ||||
|             mockEvaluator.operationAppliesTo.andCallFake(function (operation, type) { | ||||
|                 return (mockOperations[operation].appliesTo.includes(type)); | ||||
|             }); | ||||
|  | ||||
|             mockKeySelect.on.andCallFake(function (event, callback) { | ||||
|                 this.callbacks = this.callbacks || {}; | ||||
|                 this.callbacks[event] = callback; | ||||
|             }); | ||||
|  | ||||
|             mockKeySelect.triggerCallback.andCallFake(function (event, key) { | ||||
|                 this.callbacks[event](key); | ||||
|             }); | ||||
|  | ||||
|             mockManager.on.andCallFake(function (event, callback) { | ||||
|                 this.callbacks = this.callbacks || {}; | ||||
|                 this.callbacks[event] = callback; | ||||
|             }); | ||||
|  | ||||
|             mockManager.triggerCallback.andCallFake(function (event) { | ||||
|                 this.callbacks[event](); | ||||
|             }); | ||||
|  | ||||
|             mockManager.getTelemetryPropertyType.andCallFake(function (object, key) { | ||||
|                 return mockPropertyTypes[object][key]; | ||||
|             }); | ||||
|  | ||||
|             mockManager.getEvaluator.andReturn(mockEvaluator); | ||||
|         }); | ||||
|  | ||||
|         it('waits until the metadata fully loads to populate itself', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(false); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             expect(operationSelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with operations on a metadata load', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(false); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             mockManager.triggerCallback('metadata'); | ||||
|             expect(operationSelect.getSelected()).toEqual('operation1'); | ||||
|         }); | ||||
|  | ||||
|         it('populates itself with operations if metadata load is already complete', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             expect(operationSelect.getSelected()).toEqual('operation1'); | ||||
|         }); | ||||
|  | ||||
|         it('clears its selection state if the operation in its config does not apply', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); | ||||
|             expect(operationSelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('populates with the appropriate options when its linked key changes', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             mockKeySelect.triggerCallback('change', 'b'); | ||||
|             operationSelect.setSelected('operation2'); | ||||
|             expect(operationSelect.getSelected()).toEqual('operation2'); | ||||
|             operationSelect.setSelected('operation1'); | ||||
|             expect(operationSelect.getSelected()).not.toEqual('operation1'); | ||||
|         }); | ||||
|  | ||||
|         it('clears its selection on a change if the operation does not apply', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             mockKeySelect.triggerCallback('change', 'b'); | ||||
|             expect(operationSelect.getSelected()).toEqual(''); | ||||
|         }); | ||||
|  | ||||
|         it('maintains its selected state on change if the operation does apply', function () { | ||||
|             mockManager.metadataLoadCompleted.andReturn(true); | ||||
|             operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); | ||||
|             mockKeySelect.triggerCallback('change', 'c'); | ||||
|             expect(operationSelect.getSelected()).toEqual('operation1'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										42
									
								
								src/plugins/summaryWidget/test/input/PaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/plugins/summaryWidget/test/input/PaletteSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| define(['../../src/input/Palette'], function (Palette) { | ||||
|     describe('A generic Open MCT palette input', function () { | ||||
|         var palette, callbackSpy1, callbackSpy2; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); | ||||
|             callbackSpy1 = jasmine.createSpy('changeCallback1'); | ||||
|             callbackSpy2 = jasmine.createSpy('changeCallback2'); | ||||
|         }); | ||||
|  | ||||
|         it('gets the current item', function () { | ||||
|             expect(palette.getCurrent()).toEqual('item1'); | ||||
|         }); | ||||
|  | ||||
|         it('allows setting the current item', function () { | ||||
|             palette.set('item2'); | ||||
|             expect(palette.getCurrent()).toEqual('item2'); | ||||
|         }); | ||||
|  | ||||
|         it('allows registering change callbacks, and errors when an unsupported event is registered', function () { | ||||
|             expect(function () { | ||||
|                 palette.on('change', callbackSpy1); | ||||
|             }).not.toThrow(); | ||||
|             expect(function () { | ||||
|                 palette.on('someUnsupportedEvent', callbackSpy1); | ||||
|             }).toThrow(); | ||||
|         }); | ||||
|  | ||||
|         it('injects its callbacks with the new selected item on change', function () { | ||||
|             palette.on('change', callbackSpy1); | ||||
|             palette.on('change', callbackSpy2); | ||||
|             palette.set('item2'); | ||||
|             expect(callbackSpy1).toHaveBeenCalledWith('item2'); | ||||
|             expect(callbackSpy2).toHaveBeenCalledWith('item2'); | ||||
|         }); | ||||
|  | ||||
|         it('gracefully handles being set to an item not included in its set', function () { | ||||
|             palette.set('foobar'); | ||||
|             expect(palette.getCurrent()).not.toEqual('foobar'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										51
									
								
								src/plugins/summaryWidget/test/input/SelectSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/plugins/summaryWidget/test/input/SelectSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| define(['../../src/input/Select'], function (Select) { | ||||
|     describe('A select wrapper', function () { | ||||
|         var select, testOptions, callbackSpy1, callbackSpy2; | ||||
|         beforeEach(function () { | ||||
|             select = new Select(); | ||||
|             testOptions = [['item1', 'Item 1'], ['item2', 'Item 2'], ['item3', 'Item 3']]; | ||||
|             select.setOptions(testOptions); | ||||
|             callbackSpy1 = jasmine.createSpy('callbackSpy1'); | ||||
|             callbackSpy2 = jasmine.createSpy('callbackSpy2'); | ||||
|         }); | ||||
|  | ||||
|         it('gets and sets the current item', function () { | ||||
|             select.setSelected('item1'); | ||||
|             expect(select.getSelected()).toEqual('item1'); | ||||
|         }); | ||||
|  | ||||
|         it('allows adding a single new option', function () { | ||||
|             select.addOption('newOption', 'A New Option'); | ||||
|             select.setSelected('newOption'); | ||||
|             expect(select.getSelected()).toEqual('newOption'); | ||||
|         }); | ||||
|  | ||||
|         it('allows populating with a new set of options', function () { | ||||
|             select.setOptions([['newItem1', 'Item 1'], ['newItem2', 'Item 2']]); | ||||
|             select.setSelected('newItem1'); | ||||
|             expect(select.getSelected()).toEqual('newItem1'); | ||||
|         }); | ||||
|  | ||||
|         it('allows registering change callbacks, and errors when an unsupported event is registered', function () { | ||||
|             expect(function () { | ||||
|                 select.on('change', callbackSpy1); | ||||
|             }).not.toThrow(); | ||||
|             expect(function () { | ||||
|                 select.on('someUnsupportedEvent', callbackSpy1); | ||||
|             }).toThrow(); | ||||
|         }); | ||||
|  | ||||
|         it('injects its callbacks with its property and value on a change', function () { | ||||
|             select.on('change', callbackSpy1); | ||||
|             select.on('change', callbackSpy2); | ||||
|             select.setSelected('item2'); | ||||
|             expect(callbackSpy1).toHaveBeenCalledWith('item2'); | ||||
|             expect(callbackSpy2).toHaveBeenCalledWith('item2'); | ||||
|         }); | ||||
|  | ||||
|         it('gracefully handles being set to an item not included in its set', function () { | ||||
|             select.setSelected('foobar'); | ||||
|             expect(select.getSelected()).not.toEqual('foobar'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										60
									
								
								src/plugins/telemetryMean/plugin.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										60
									
								
								src/plugins/telemetryMean/plugin.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| define([ | ||||
|     './src/TelemetryMeanProvider', | ||||
|     './src/TelemetryMeanActionDecorator' | ||||
| ],  | ||||
|     function ( | ||||
|         TelemetryMeanProvider, TelemetryMeanActionDecorator) { | ||||
|     var DEFAULT_SAMPLES = 10; | ||||
|  | ||||
|     function plugin() { | ||||
|         return function install(openmct) { | ||||
|             openmct.types.addType('telemetry-mean', { | ||||
|                 name: 'Telemetry Filter', | ||||
|                 description: 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', | ||||
|                 creatable: true, | ||||
|                 cssClass: 'icon-telemetry', | ||||
|                 initialize: function (domainObject) { | ||||
|                     domainObject.samples = DEFAULT_SAMPLES; | ||||
|                     domainObject.telemetry = { | ||||
|                         values: [ | ||||
|                             { | ||||
|                                 key: "utc", | ||||
|                                 name: "Time", | ||||
|                                 format: "utc", | ||||
|                                 hints: { | ||||
|                                     domain: 1 | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 key: "value", | ||||
|                                 name: "Value", | ||||
|                                 hints: { | ||||
|                                     range: 1 | ||||
|                                 } | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 }, | ||||
|                 form: [ | ||||
|                     { | ||||
|                         "key": "telemetryPoint", | ||||
|                         "name": "Telemetry Point", | ||||
|                         "control": "textfield", | ||||
|                         "required": true, | ||||
|                         "cssClass": "l-input-lg" | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "samples", | ||||
|                         "name": "Samples to Average", | ||||
|                         "control": "textfield", | ||||
|                         "required": true, | ||||
|                         "cssClass": "l-input-sm" | ||||
|                     } | ||||
|                 ]     | ||||
|             }); | ||||
|             openmct.telemetry.addProvider(new TelemetryMeanProvider(openmct)); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return plugin; | ||||
| }); | ||||
| @@ -0,0 +1,71 @@ | ||||
| define([], function () { | ||||
|     function TelemetryMeanActionDecorator (openmct, actionService) { | ||||
|         this.actionService = actionService; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         [ | ||||
|             'decorateAction', | ||||
|             'getActions', | ||||
|             'updateTelemetryFromLinkedObject' | ||||
|         ].forEach(function (name) { | ||||
|             this[name] = this[name].bind(this); | ||||
|         }.bind(this)) | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanActionDecorator.prototype.decorateAction = function (action) { | ||||
|         function update(object) { | ||||
|             var domainObject = object || action.domainObject; | ||||
|             return this.updateTelemetryFromLinkedObject(object) | ||||
|             .then(function (modelWithTelemetry) { | ||||
|                 return this.mutate(domainObject, modelWithTelemetry); | ||||
|             }.bind(this)); | ||||
|         } | ||||
|  | ||||
|         if (action.getMetadata && action.getMetadata().key === 'properties' || action.getMetadata().key === 'create'){ | ||||
|             var oldPerform = action.perform.bind(action); | ||||
|             action.perform = function () { | ||||
|                 return oldPerform().then(update.bind(this), update.bind(this)); | ||||
|             }.bind(this); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanActionDecorator.prototype.mutate = function (domainObject, model) { | ||||
|         return domainObject.useCapability('mutation', function () { | ||||
|             return model | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanActionDecorator.prototype.getActions = function () { | ||||
|         var actions = this.actionService.getActions.apply(this.actionService, arguments); | ||||
|         actions.forEach(this.decorateAction); | ||||
|         return actions;       | ||||
|     }; | ||||
|  | ||||
|     TelemetryMeanActionDecorator.prototype.updateTelemetryFromLinkedObject = function (domainObject) { | ||||
|         var model = domainObject.getModel(); | ||||
|         var telemetryPoint = model.telemetryPoint; | ||||
|         var telemetryApi = this.openmct.telemetry; | ||||
|  | ||||
|         if (telemetryPoint) { | ||||
|             return this.openmct.objects.get(telemetryPoint).then(function (referencedObject) { | ||||
|                 if (referencedObject.type !== 'unknown') { | ||||
|                     var keysForRanges = telemetryApi.getMetadata(referencedObject).valuesForHints(['range']) | ||||
|                         .map(function (metadatum) { | ||||
|                             return metadatum.source; | ||||
|                         }); | ||||
|  | ||||
|                     model.telemetry.values = referencedObject.telemetry.values.map(function (value) { | ||||
|                         if (keysForRanges.indexOf(value.source) !== -1) { | ||||
|                             value.name = value.name + " (Mean)"; | ||||
|                         } | ||||
|                         return value; | ||||
|                     }); | ||||
|                 } | ||||
|                 return model; | ||||
|             }.bind(this)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return TelemetryMeanActionDecorator; | ||||
|  | ||||
| }); | ||||
							
								
								
									
										112
									
								
								src/plugins/telemetryMean/src/TelemetryMeanProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/plugins/telemetryMean/src/TelemetryMeanProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|  | ||||
|     function TelemetryMeanProvider(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.subscriptionBuffer = []; | ||||
|         this.lastNValues = []; | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         return domainObject.type === 'telemetry-mean'; | ||||
|     }; | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.supportsRequest = function () { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.supportsSubscribe = | ||||
|         TelemetryMeanProvider.prototype.canProvideTelemetry; | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var promiseForObject = this.getWrappedObject(domainObject) | ||||
|         return this.subscribeToWrappedObject(promiseForObject, callback); | ||||
|     }; | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.getWrappedObject = function (domainObject) { | ||||
|         var objectId = domainObject.telemetryPoint; | ||||
|         return this.openmct.objects.get(objectId); | ||||
|     }; | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.subscribeToWrappedObject = function (promiseForObject, callback) { | ||||
|         var wrappedUnsubscribe; | ||||
|         var unsubscribeCalled = false; | ||||
|  | ||||
|         promiseForObject.then(function subscribe(wrappedObject) { | ||||
|             if (!unsubscribeCalled && wrappedObject){ | ||||
|                 wrappedUnsubscribe = this.subscribeToMeanValues(wrappedObject, callback); | ||||
|             } | ||||
|         }.bind(this)); | ||||
|          | ||||
|         return function unsubscribe(){ | ||||
|             unsubscribeCalled = true; | ||||
|             if (wrappedUnsubscribe !== undefined) { | ||||
|                 wrappedUnsubscribe(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.subscribeToMeanValues = function (object, callback) { | ||||
|         var telemetryApi = this.openmct.telemetry; | ||||
|         var lastNData = []; | ||||
|         var rangeKey = telemetryApi.getMetadata(object).valuesForHints(['range']) | ||||
|             .map(function (metadatum) { | ||||
|                 return metadatum.source; | ||||
|             } | ||||
|         )[0]; | ||||
|  | ||||
|         return telemetryApi.subscribe(object, function (telemetryDatum) { | ||||
|  | ||||
|             lastNData.push(telemetryDatum); | ||||
|             if (lastNData.length > object.samples) { | ||||
|                 lastNData.shift(); | ||||
|             } | ||||
|  | ||||
|             var meanDatum = this.calculateMeansForDatum(telemetryDatum, rangeKey, lastNData); | ||||
|             callback(meanDatum); | ||||
|  | ||||
|         }.bind(this)); | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.calculateMeansForDatum = function (telemetryDatum, keyToMean, lastNData) { | ||||
|         var meanDatum = { | ||||
|             'utc': telemetryDatum['utc'], | ||||
|             'value': this.calculateMean(lastNData, keyToMean) | ||||
|         } | ||||
|         return meanDatum; | ||||
|     } | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.calculateMean = function (lastNData, valueToMean) { | ||||
|         return lastNData.reduce(function (sum, datum){ | ||||
|             return sum + datum[valueToMean]; | ||||
|         }, 0) / lastNData.length; | ||||
|     }; | ||||
|  | ||||
|     TelemetryMeanProvider.prototype.request = function (domainObject, request) { | ||||
|         throw "Historical requests not supported for Telemetry Averager"; | ||||
|     }; | ||||
|  | ||||
|     return TelemetryMeanProvider; | ||||
| }); | ||||
| @@ -28,6 +28,7 @@ define([], function () { | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     function ViewRegistry() { | ||||
|         this.next_id = 0; | ||||
|         this.providers = []; | ||||
|     } | ||||
|  | ||||
| @@ -40,7 +41,8 @@ define([], function () { | ||||
|      */ | ||||
|     ViewRegistry.prototype.get = function (item) { | ||||
|         return this.providers.filter(function (provider) { | ||||
|             return provider.canView(item); | ||||
|             return typeof provider.canView(item) !== 'undefined' && | ||||
|                 provider.canView(item) !== false; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
| @@ -52,9 +54,21 @@ define([], function () { | ||||
|      * @memberof module:openmct.ViewRegistry# | ||||
|      */ | ||||
|     ViewRegistry.prototype.addProvider = function (provider) { | ||||
|         provider.vpid = this.next_id++; | ||||
|         this.providers.push(provider); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Used internally to support seamless usage of new views with old | ||||
|      * views. | ||||
|      * @private | ||||
|      */ | ||||
|     ViewRegistry.prototype.getByVPID = function (vpid) { | ||||
|         return this.providers.filter(function (p) { | ||||
|             return p.vpid === vpid; | ||||
|         })[0]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A View is used to provide displayable content, and to react to | ||||
|      * associated life cycle events. | ||||
| @@ -91,6 +105,11 @@ define([], function () { | ||||
|      * Exposes types of views in Open MCT. | ||||
|      * | ||||
|      * @interface ViewProvider | ||||
|      * @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) | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|  | ||||
| @@ -107,8 +126,11 @@ define([], function () { | ||||
|      * @memberof module:openmct.ViewProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object | ||||
|      *        to be viewed | ||||
|      * @returns {boolean} true if this domain object can be viewed using | ||||
|      *          this provider | ||||
|      * @returns {Number|boolean} if this returns `false`, then the view does | ||||
|      *          not apply to the object.  If it returns true or any number, then | ||||
|      *          it applies to this object.  If multiple views could apply | ||||
|      *          to an object, the view that returns the lowest number will be | ||||
|      *          the default view. | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
| @@ -126,27 +148,6 @@ define([], function () { | ||||
|      * @returns {module:openmct.View} a view of this domain object | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Get metadata associated with this view provider. This may be used | ||||
|      * to populate the user interface with options associated with this | ||||
|      * view provider. | ||||
|      * | ||||
|      * @method metadata | ||||
|      * @memberof module:openmct.ViewProvider# | ||||
|      * @returns {module:openmct.ViewProvider~ViewMetadata} view metadata | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * @typedef ViewMetadata | ||||
|      * @memberof module:openmct.ViewProvider~ | ||||
|      * @property {string} name the human-readable name of this view | ||||
|      * @property {string} key a machine-readable name for 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) | ||||
|      */ | ||||
|  | ||||
|     return ViewRegistry; | ||||
|  | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user