mirror of
https://github.com/nasa/openmct.git
synced 2025-06-24 18:25:19 +00:00
Compare commits
159 Commits
master
...
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