mirror of
https://github.com/nasa/openmct.git
synced 2024-12-21 06:03:08 +00:00
Summary Widgets (#1668)
* [ViewAPI] Update view API with more support Update view provider to allow metadata definitions and to play nicely with old style views. Spec out some updates to ViewProviders and ViewRegistry to support further use of views. * [Summary Widgets] Add summary widgets Add a summary widget domain object type Implement basic interface and style configuration for rules * [Summary Widgets] Implementation for Rules Add rule configuration inputs, populated with domain objects, metadata, and appropriate operations for a given type * [Inputs] Add implementation for icon palette Issue #1644 Wire up icon palette inputs to widget, and make icon class a persistable property of a rule * [Summary Widgets] Implementation for conditions Support configuring and persisting multiple conditions per rule Issue #1644 * [Summary Widgets] Generate Rule Descriptions Dynamically update the rule description based on the current state of the rules' conditions * [Summary Widgets] 'Any/All Telemetry' in conditions Add UI and implemenetion for evaluating any telemetry or all telemetry in an individual condition. Add related unit tests. * [Summary Widgets] Rule Reorders Implement drag and drop rule reorders using the native HTML5 API * [Summary Widget] Test Data Issue #1644 Add user-configurable mock data to test rules. Modify evaluator to gracefully handle uninitialzed test data points. * [Summary Widgets] Edit Mode Enable edit mode for summary widgets, and make configuration interface visible only when the user has entered edit mode Fix collision between widget palettes and other interfaces where palettes would permanently hide other menus * [Summary Widgets] UI for scripted conditions * [Sumamry Widgets] Destroy Implement destroy * [Summary Widgets] Cleanup Remove unnecessary persist calls in Rule.js. Remove generateDescription from refreshConditions and add it after refreshConditions to initCondition and deleteCondition Throw error when unsupported callback is passed in condition.js, return summary widget instance in plugin.js instead of wrapping in new object for view Add request properties to telemetry request for providers that support it Remove check for editing when persisting, in SummaryWidget.js
This commit is contained in:
parent
5a49ac16b1
commit
4b07930305
@ -59,7 +59,7 @@ define([
|
|||||||
if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) {
|
if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) {
|
||||||
workerRequest[prop] = domainObject.telemetry[prop];
|
workerRequest[prop] = domainObject.telemetry[prop];
|
||||||
}
|
}
|
||||||
if (request.hasOwnProperty(prop)) {
|
if (request && request.hasOwnProperty(prop)) {
|
||||||
workerRequest[prop] = request[prop];
|
workerRequest[prop] = request[prop];
|
||||||
}
|
}
|
||||||
if (!workerRequest[prop]) {
|
if (!workerRequest[prop]) {
|
||||||
|
@ -121,7 +121,7 @@
|
|||||||
<h2>Palettes</h2>
|
<h2>Palettes</h2>
|
||||||
<div class="cols cols1-1">
|
<div class="cols cols1-1">
|
||||||
<div class="col">
|
<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>
|
<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>
|
</div>
|
||||||
<mct-example><div style="height: 220px" title="Ignore me, I'm just here to provide space for this example.">
|
<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">
|
<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="l-click-area" ng-click="toggle.toggle()"></span>
|
||||||
<span class="color-swatch" style="background: rgb(255, 0, 0);"></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-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>
|
<span class="l-palette-item-label">None</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="l-palette-row">
|
<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 class="l-palette-item s-palette-item" style="background: rgb(255, 255, 255);"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="l-palette-row">
|
<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(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(240, 160, 72);"></div>
|
||||||
<div class="l-palette-item s-palette-item" style="background: rgb(255, 248, 96);"></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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<title></title>
|
<title></title>
|
||||||
<script src="bower_components/requirejs/require.js">
|
<script src="bower_components/requirejs/require.js"> </script>
|
||||||
</script>
|
|
||||||
<script>
|
<script>
|
||||||
var THIRTY_MINUTES = 30 * 60 * 1000;
|
var THIRTY_MINUTES = 30 * 60 * 1000;
|
||||||
|
|
||||||
@ -50,7 +49,7 @@
|
|||||||
name: "Fixed",
|
name: "Fixed",
|
||||||
timeSystem: 'utc',
|
timeSystem: 'utc',
|
||||||
bounds: {
|
bounds: {
|
||||||
start: Date.now() - 30 * 60 * 1000,
|
start: Date.now() - THIRTY_MINUTES,
|
||||||
end: Date.now()
|
end: Date.now()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -65,6 +64,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
|
openmct.install(openmct.plugins.SummaryWidget());
|
||||||
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
|
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
|
||||||
openmct.time.timeSystem('utc');
|
openmct.time.timeSystem('utc');
|
||||||
openmct.start();
|
openmct.start();
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<div class="abs top-bar">
|
<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 class="hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='abs editor'>
|
<div class='abs editor'>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
<div class="l-message"
|
<div class="l-message"
|
||||||
ng-class="'message-severity-' + ngModel.severity">
|
ng-class="'message-severity-' + ngModel.severity">
|
||||||
<div class="ui-symbol type-icon message-type"></div>
|
<div class="w-message-contents">
|
||||||
<div class="message-contents">
|
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="title">{{ngModel.title}}</div>
|
<div class="title">{{ngModel.title}}</div>
|
||||||
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<div class="message-action">
|
<div class="message-action">
|
||||||
{{ngModel.actionText}}
|
{{ngModel.actionText}}
|
||||||
@ -25,8 +24,6 @@
|
|||||||
ng-click="ngModel.primaryOption.callback()">
|
ng-click="ngModel.primaryOption.callback()">
|
||||||
{{ngModel.primaryOption.label}}
|
{{ngModel.primaryOption.label}}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<mct-container key="overlay" class="t-message-list">
|
<mct-container key="overlay">
|
||||||
<div class="message-contents">
|
<div class="t-message-list">
|
||||||
<div class="abs top-bar">
|
<div class="top-bar">
|
||||||
<div class="title">{{ngModel.dialog.title}}</div>
|
<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 ||
|
<div class="hint">Displaying {{ngModel.dialog.messages.length}} message<span ng-show="ngModel.dialog.messages.length > 1 ||
|
||||||
ngModel.dialog.messages.length == 0">s</span>
|
ngModel.dialog.messages.length == 0">s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="abs message-body">
|
<div class="w-messages">
|
||||||
<mct-include
|
<mct-include
|
||||||
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
|
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
|
||||||
key="'message'" ng-model="msg.model"></mct-include>
|
key="'message'" ng-model="msg.model"></mct-include>
|
||||||
</div>
|
</div>
|
||||||
<div class="abs bottom-bar">
|
<div class="bottom-bar">
|
||||||
<a ng-repeat="dialogAction in ngModel.dialog.actions"
|
<a ng-repeat="dialogAction in ngModel.dialog.actions"
|
||||||
class="s-button major"
|
class="s-button major"
|
||||||
ng-click="dialogAction.action()">
|
ng-click="dialogAction.action()">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
-->
|
-->
|
||||||
<mct-container key="overlay">
|
<mct-container key="overlay">
|
||||||
<div class="abs top-bar">
|
<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 class="hint">{{ngModel.dialog.hint}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='abs editor'>
|
<div class='abs editor'>
|
||||||
|
@ -137,6 +137,11 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
&.holder:not(:last-child) { margin-bottom: $interiorMarginLg; }
|
&.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); }
|
.flex-container { @include flex-direction(column); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +180,20 @@ a.disabled {
|
|||||||
@include ellipsize();
|
@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,
|
.scrolling,
|
||||||
.scroll {
|
.scroll {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
/********************************* CONTROLS */
|
/********************************* CONTROLS */
|
||||||
@import "controls/breadcrumb";
|
@import "controls/breadcrumb";
|
||||||
@import "controls/buttons";
|
@import "controls/buttons";
|
||||||
@import "controls/color-palette";
|
@import "controls/palette";
|
||||||
@import "controls/controls";
|
@import "controls/controls";
|
||||||
@import "controls/lists";
|
@import "controls/lists";
|
||||||
@import "controls/menus";
|
@import "controls/menus";
|
||||||
@ -80,3 +80,4 @@
|
|||||||
@import "autoflow";
|
@import "autoflow";
|
||||||
@import "features/imagery";
|
@import "features/imagery";
|
||||||
@import "features/time-display";
|
@import "features/time-display";
|
||||||
|
@import "widgets";
|
||||||
|
306
platform/commonUI/general/res/sass/_widgets.scss
Normal file
306
platform/commonUI/general/res/sass/_widgets.scss
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.l-rule-action-buttons-wrapper {
|
||||||
|
.t-delete {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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; }
|
input[type="text"].lg { width: 100% !important; }
|
||||||
.l-input-med input[type="text"],
|
.l-input-med input[type="text"],
|
||||||
input[type="text"].med { width: 200px !important; }
|
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"],
|
.l-numeric input[type="text"],
|
||||||
input[type="text"].numeric { text-align: right; }
|
input[type="text"].numeric { text-align: right; }
|
||||||
|
|
||||||
@ -317,14 +317,10 @@ input[type="text"].s-input-inline,
|
|||||||
.select {
|
.select {
|
||||||
@include btnSubtle($bg: $colorSelectBg);
|
@include btnSubtle($bg: $colorSelectBg);
|
||||||
@extend .icon-arrow-down; // Context arrow
|
@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;
|
display: inline-block;
|
||||||
padding: 0 $interiorMargin;
|
padding: 0 $interiorMargin;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: $formInputH;
|
|
||||||
select {
|
select {
|
||||||
@include appearance(none);
|
@include appearance(none);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -340,11 +336,13 @@ input[type="text"].s-input-inline,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:before {
|
&:before {
|
||||||
pointer-events: none;
|
@include transform(translateY(-50%));
|
||||||
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
|
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
|
||||||
display: block;
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $interiorMargin; top: 0;
|
right: $interiorMargin;
|
||||||
|
top: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,8 +394,7 @@ input[type="text"].s-input-inline,
|
|||||||
.l-elem-wrapper {
|
.l-elem-wrapper {
|
||||||
mct-representation {
|
mct-representation {
|
||||||
// Holds the context-available item
|
// Holds the context-available item
|
||||||
// Must have min-width to make flex work properly
|
// Must have min-width to make flex work properly in Safari
|
||||||
// in Safari
|
|
||||||
min-width: 0.7em;
|
min-width: 0.7em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -563,7 +560,6 @@ input[type="text"].s-input-inline,
|
|||||||
height: $h;
|
height: $h;
|
||||||
margin-top: 1 + floor($h/2) * -1;
|
margin-top: 1 + floor($h/2) * -1;
|
||||||
@include btnSubtle(pullForward($colorBtnBg, 10%));
|
@include btnSubtle(pullForward($colorBtnBg, 10%));
|
||||||
//border-radius: 50% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin sliderKnobRound() {
|
@mixin sliderKnobRound() {
|
||||||
@ -578,7 +574,6 @@ input[type="text"].s-input-inline,
|
|||||||
|
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
// HTML5 range inputs
|
// HTML5 range inputs
|
||||||
|
|
||||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||||
background: transparent; /* Otherwise white in Chrome */
|
background: transparent; /* Otherwise white in Chrome */
|
||||||
&:focus {
|
&: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 */
|
/******************************************************** BROWSER ELEMENTS */
|
||||||
body.desktop {
|
body.desktop {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -29,23 +29,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 16px; //120%;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-label {
|
.title-label {
|
||||||
margin-left: $interiorMarginSm;
|
margin-left: $interiorMarginSm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-swatch,
|
||||||
.color-swatch {
|
.color-swatch {
|
||||||
// Used in color menu buttons in toolbar
|
// Used in color menu buttons in toolbar
|
||||||
$d: 10px;
|
$d: 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid rgba($colorBtnFg, 0.2);
|
border: 1px solid rgba($colorBtnFg, 0.2);
|
||||||
height: $d;
|
height: $d; width: $d;
|
||||||
width: $d;
|
line-height: $d;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: $interiorMarginSm;
|
margin-left: $interiorMarginSm;
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
|
&:not(.no-selection) {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
/******************************************************************* STATUS BLOCK ELEMS */
|
||||||
@mixin statusBannerColors($bg, $fg: $colorStatusFg) {
|
@mixin statusBannerColors($bg, $fg: $colorStatusFg) {
|
||||||
$bgPb: 30%;
|
$bgPb: 30%;
|
||||||
$bgPbD: 10%;
|
$bgPbD: 10%;
|
||||||
@ -140,7 +140,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for messages and message banners */
|
/******************************************************************* MESSAGE BANNERS */
|
||||||
.message {
|
.message {
|
||||||
&.block {
|
&.block {
|
||||||
border-radius: $basicCr;
|
border-radius: $basicCr;
|
||||||
@ -196,7 +196,6 @@
|
|||||||
padding: 0 $interiorMargin;
|
padding: 0 $interiorMargin;
|
||||||
}
|
}
|
||||||
.close {
|
.close {
|
||||||
//@include test(red, 0.7);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -240,132 +239,147 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin messageBlock($iconW: 32px) {
|
/******************************************************************* MESSAGES */
|
||||||
.type-icon.message-type {
|
|
||||||
|
/* 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);
|
@include txtShdw($shdwStatusIc);
|
||||||
@extend .icon-bell;
|
@extend .icon-bell;
|
||||||
color: $colorStatusDefault;
|
color: $colorStatusDefault;
|
||||||
font-size: $iconW;
|
font-size: $iconW;
|
||||||
padding: 1px;
|
|
||||||
width: $iconW + 2;
|
width: $iconW + 2;
|
||||||
|
margin-right: $interiorMarginLg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-severity-info .type-icon.message-type {
|
&.message-severity-info:before {
|
||||||
@extend .icon-info;
|
@extend .icon-info;
|
||||||
color: $colorInfo;
|
color: $colorInfo;
|
||||||
}
|
}
|
||||||
.message-severity-alert .type-icon.message-type {
|
|
||||||
@extend .icon-bell;
|
&.message-severity-alert:before {
|
||||||
color: $colorWarningLo;
|
color: $colorWarningLo;
|
||||||
}
|
}
|
||||||
.message-severity-error .type-icon.message-type {
|
|
||||||
|
&.message-severity-error:before {
|
||||||
@extend .icon-alert-rect;
|
@extend .icon-alert-rect;
|
||||||
color: $colorWarningHi;
|
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 {
|
|
||||||
@include display(flex);
|
.w-message-contents {
|
||||||
@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);
|
@include flex(1 1 auto);
|
||||||
margin-left: $overlayMargin;
|
@include display(flex);
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.top-bar,
|
|
||||||
.message-body {
|
|
||||||
margin-bottom: $interiorMarginLg * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Message as singleton
|
|
||||||
.t-message-single {
|
|
||||||
@include messageBlock(80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.desktop .t-message-single {
|
|
||||||
.l-message,
|
|
||||||
.bottom-bar {
|
|
||||||
@include absPosDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-bar {
|
|
||||||
top: auto;
|
|
||||||
height: $ovrFooterH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include phonePortrait {
|
|
||||||
.t-message-single {
|
|
||||||
.l-message {
|
|
||||||
@include flex-direction(column);
|
@include flex-direction(column);
|
||||||
.message-contents { margin-left: 0; }
|
|
||||||
}
|
> div,
|
||||||
.type-icon.message-type {
|
> span {
|
||||||
margin-bottom: $interiorMarginLg;
|
//@include test(red);
|
||||||
width: 100%;
|
margin-bottom: $interiorMargin;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-bar {
|
.message-body {
|
||||||
text-align: center !important;
|
@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-top: $interiorMargin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages in list
|
// In a list
|
||||||
.t-message-list {
|
.t-message-list {
|
||||||
@include messageBlock(32px);
|
@include absPosDefault();
|
||||||
|
@include display(flex);
|
||||||
|
@include flex-direction(column);
|
||||||
|
|
||||||
.message-contents {
|
> div,
|
||||||
|
> span {
|
||||||
|
margin-bottom: $interiorMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-messages {
|
||||||
|
@include flex(1 1 100%);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: $interiorMargin;
|
||||||
|
}
|
||||||
|
// Each message
|
||||||
.l-message {
|
.l-message {
|
||||||
border-radius: $controlCr;
|
border-radius: $controlCr;
|
||||||
background: rgba($colorOvrFg, 0.1);
|
background: rgba($colorOvrFg, 0.1);
|
||||||
margin-bottom: $interiorMargin;
|
margin-bottom: $interiorMargin;
|
||||||
padding: $interiorMarginLg;
|
.hint,
|
||||||
|
|
||||||
.message-contents,
|
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
position: relative;
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-contents {
|
}
|
||||||
font-size: 0.9em;
|
|
||||||
margin-left: $interiorMarginLg;
|
|
||||||
.message-action { color: pushBack($colorOvrFg, 20%); }
|
|
||||||
.bottom-bar { text-align: left; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar,
|
@include phonePortrait {
|
||||||
.message-body {
|
.t-message-single .l-message,
|
||||||
|
.t-message-single.l-message {
|
||||||
|
@include flex-direction(column);
|
||||||
|
&:before {
|
||||||
|
margin-right: 0;
|
||||||
margin-bottom: $interiorMarginLg;
|
margin-bottom: $interiorMarginLg;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
text-align: center;
|
||||||
|
.s-button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.desktop .t-message-list {
|
body.desktop .t-message-list {
|
||||||
.message-contents .l-message { margin-right: $interiorMarginLg; }
|
.w-message-contents { padding-right: $interiorMargin; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alert elements in views
|
// Alert elements in views
|
||||||
|
@ -19,11 +19,10 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
.l-color-palette {
|
.l-palette {
|
||||||
$d: 16px;
|
$d: 16px;
|
||||||
$colorsPerRow: 10;
|
$colorsPerRow: 10;
|
||||||
$m: 1;
|
$m: 1;
|
||||||
$colorSelectedColor: #fff;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: $interiorMargin !important;
|
padding: $interiorMargin !important;
|
||||||
@ -33,46 +32,41 @@
|
|||||||
line-height: $d;
|
line-height: $d;
|
||||||
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
|
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
|
||||||
|
|
||||||
|
&.l-option-row {
|
||||||
|
margin-bottom: $interiorMargin;
|
||||||
|
.s-palette-item {
|
||||||
|
border-color: $colorPaletteFg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.l-palette-item {
|
.l-palette-item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@include txtShdwSubtle(0.8);
|
|
||||||
@include trans-prop-nice-fade(0.25s);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
color: $colorSelectedColor;
|
|
||||||
display: block;
|
display: block;
|
||||||
float: left;
|
float: left;
|
||||||
height: $d; width: $d;
|
height: $d; width: $d;
|
||||||
line-height: $d * 0.9;
|
line-height: $d * 0.9;
|
||||||
margin: 0 ($m * 1px) ($m * 1px) 0;
|
margin: 0 ($m * 1px) ($m * 1px) 0;
|
||||||
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
&:before {
|
|
||||||
// Check mark for selected items
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-palette-item {
|
.s-palette-item {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: $colorPaletteFg;
|
||||||
|
text-shadow: $shdwPaletteFg;
|
||||||
|
@include trans-prop-nice-fade(0.25s);
|
||||||
&:hover {
|
&:hover {
|
||||||
@include trans-prop-nice-fade(0);
|
@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 {
|
.l-palette-item-label {
|
||||||
margin-left: $interiorMargin;
|
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.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
.section-header {
|
.section-header {
|
||||||
|
border-radius: $basicCr;
|
||||||
|
background: $colorFormSectionHeader;
|
||||||
|
color: lighten($colorBodyFg, 20%);
|
||||||
|
font-size: inherit;
|
||||||
|
margin: $interiorMargin 0;
|
||||||
|
padding: $formTBPad $formLRPad;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
.view-control {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $interiorMargin;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.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 {
|
.form-row {
|
||||||
$m: $interiorMargin;
|
$m: $interiorMargin;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -57,9 +60,6 @@
|
|||||||
margin-bottom: $interiorMarginLg * 2;
|
margin-bottom: $interiorMarginLg * 2;
|
||||||
padding: $formTBPad 0;
|
padding: $formTBPad 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
//&ng-form {
|
|
||||||
// display: block;
|
|
||||||
//}
|
|
||||||
|
|
||||||
&.first {
|
&.first {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@ -171,3 +171,106 @@
|
|||||||
padding: $interiorMargin;
|
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
|
// Dialog boxes, size constrained and centered in desktop/tablet
|
||||||
&.l-dialog {
|
&.l-dialog {
|
||||||
|
font-size: 0.8rem;
|
||||||
.s-button {
|
.s-button {
|
||||||
&:not(.major) {
|
&:not(.major) {
|
||||||
@include btnSubtle($bg: $colorOvrBtnBg, $bgHov: pullForward($colorOvrBtnBg, 10%), $fg: $colorOvrBtnFg, $fgHov: $colorOvrBtnFg, $ic: $colorOvrBtnFg, $icHov: $colorOvrBtnFg);
|
@include btnSubtle($bg: $colorOvrBtnBg, $bgHov: pullForward($colorOvrBtnBg, 10%), $fg: $colorOvrBtnFg, $fgHov: $colorOvrBtnFg, $ic: $colorOvrBtnFg, $icHov: $colorOvrBtnFg);
|
||||||
@ -125,9 +126,9 @@
|
|||||||
@include containerSubtle($colorOvrBg, $colorOvrFg);
|
@include containerSubtle($colorOvrBg, $colorOvrFg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.dialog-title {
|
||||||
@include ellipsize();
|
@include ellipsize();
|
||||||
font-size: 1.2em;
|
font-size: 1.5em;
|
||||||
line-height: 120%;
|
line-height: 120%;
|
||||||
margin-bottom: $interiorMargin;
|
margin-bottom: $interiorMargin;
|
||||||
}
|
}
|
||||||
|
@ -52,21 +52,13 @@ ul.tree {
|
|||||||
|
|
||||||
.view-control {
|
.view-control {
|
||||||
color: $colorItemTreeVC;
|
color: $colorItemTreeVC;
|
||||||
font-size: 0.75em;
|
|
||||||
margin-right: $interiorMargin;
|
margin-right: $interiorMargin;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
width: $treeVCW;
|
width: $treeVCW;
|
||||||
|
&:before { display: none; }
|
||||||
&.has-children {
|
&.has-children {
|
||||||
&:before {
|
&:before { display: block; }
|
||||||
position: absolute;
|
|
||||||
@include trans-prop-nice(transform, 100ms);
|
|
||||||
content: "\e904";
|
|
||||||
@include transform-origin(center);
|
|
||||||
}
|
|
||||||
&.expanded:before {
|
|
||||||
@include transform(rotate(90deg));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,8 @@
|
|||||||
|
|
||||||
&.t-object-type-timer,
|
&.t-object-type-timer,
|
||||||
&.t-object-type-clock,
|
&.t-object-type-clock,
|
||||||
&.t-object-type-hyperlink {
|
&.t-object-type-hyperlink,
|
||||||
|
&.t-object-type-summary-widget {
|
||||||
// Hide the right side buttons for objects where they don't make sense
|
// Hide the right side buttons for objects where they don't make sense
|
||||||
// Note that this will hide the view Switcher button if applied
|
// Note that this will hide the view Switcher button if applied
|
||||||
// to an object that has it.
|
// to an object that has it.
|
||||||
@ -126,13 +127,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/********************************************************** OBJECT TYPES */
|
/********************************************************** OBJECT TYPES */
|
||||||
.t-object-type-hyperlink {
|
.t-object-type-hyperlink,
|
||||||
|
.t-object-type-summary-widget {
|
||||||
.object-holder {
|
.object-holder {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.w-summary-widget,
|
||||||
|
.l-summary-widget,
|
||||||
.l-hyperlink.s-button {
|
.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;
|
@extend .abs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-summary-widget,
|
||||||
|
.l-hyperlink.s-button {
|
||||||
.label {
|
.label {
|
||||||
@include ellipsize();
|
@include ellipsize();
|
||||||
@include transform(translateY(-50%));
|
@include transform(translateY(-50%));
|
||||||
|
@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
|
|||||||
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
||||||
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
|
$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
|
// About Screen
|
||||||
$colorAboutLink: #84b3ff;
|
$colorAboutLink: #84b3ff;
|
||||||
|
|
||||||
|
@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
|
|||||||
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
||||||
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
|
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
|
||||||
|
|
||||||
|
// Palettes
|
||||||
|
$colorPaletteFg: pullForward($colorMenuBg, 30%);
|
||||||
|
$colorPaletteSelected: #333;
|
||||||
|
$shdwPaletteFg: none;
|
||||||
|
$shdwPaletteSelected: inset 0 0 0 1px #fff;
|
||||||
|
|
||||||
// About Screen
|
// About Screen
|
||||||
$colorAboutLink: #84b3ff;
|
$colorAboutLink: #84b3ff;
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
this source code distribution or the Licensing information page available
|
this source code distribution or the Licensing information page available
|
||||||
at runtime from the About dialog for additional information.
|
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="abs object-browse-bar l-flex-row">
|
||||||
<div class="left flex-elem l-flex-row grows">
|
<div class="left flex-elem l-flex-row grows">
|
||||||
<mct-representation
|
<mct-representation
|
||||||
|
@ -24,21 +24,22 @@
|
|||||||
|
|
||||||
<span class="l-click-area" ng-click="toggle.toggle()"></span>
|
<span class="l-click-area" ng-click="toggle.toggle()"></span>
|
||||||
<span class="color-swatch"
|
<span class="color-swatch"
|
||||||
|
ng-class="{'no-selection':ngModel[field] === 'transparent'}"
|
||||||
ng-style="{
|
ng-style="{
|
||||||
background: ngModel[field]
|
'background-color': ngModel[field]
|
||||||
}">
|
}">
|
||||||
</span>
|
</span>
|
||||||
<span class="title-label" ng-if="structure.text">
|
<span class="title-label" ng-if="structure.text">
|
||||||
{{structure.text}}
|
{{structure.text}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="menu l-color-palette"
|
<div class="menu l-palette l-color-palette"
|
||||||
ng-controller="ColorController as colors"
|
ng-controller="ColorController as colors"
|
||||||
ng-show="toggle.isActive()">
|
ng-show="toggle.isActive()">
|
||||||
<div
|
<div
|
||||||
class="l-palette-row l-option-row"
|
class="l-palette-row l-option-row"
|
||||||
ng-if="!structure.mandatory">
|
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'">
|
ng-click="ngModel[field] = 'transparent'">
|
||||||
</div>
|
</div>
|
||||||
<span class="l-palette-item-label">None</span>
|
<span class="l-palette-item-label">None</span>
|
||||||
@ -46,7 +47,7 @@
|
|||||||
<div
|
<div
|
||||||
class="l-palette-row"
|
class="l-palette-row"
|
||||||
ng-repeat="group in colors.groups()">
|
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-repeat="color in group"
|
||||||
ng-style="{ background: color }"
|
ng-style="{ background: color }"
|
||||||
ng-click="ngModel[field] = color">
|
ng-click="ngModel[field] = color">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<a class="close icon-x-in-circle"></a>
|
<a class="close icon-x-in-circle"></a>
|
||||||
<div class="abs inner-holder contents">
|
<div class="abs inner-holder contents">
|
||||||
<div class="abs top-bar">
|
<div class="abs top-bar">
|
||||||
<div class="title"></div>
|
<div class="dialog-title"></div>
|
||||||
<div class="hint"></div>
|
<div class="hint"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class='abs editor'>
|
<div class='abs editor'>
|
||||||
|
@ -28,6 +28,7 @@ define([
|
|||||||
'./timeConductor/plugin',
|
'./timeConductor/plugin',
|
||||||
'../../example/imagery/plugin',
|
'../../example/imagery/plugin',
|
||||||
'../../platform/import-export/bundle',
|
'../../platform/import-export/bundle',
|
||||||
|
'./summaryWidget/plugin',
|
||||||
'./URLIndicatorPlugin/URLIndicatorPlugin'
|
'./URLIndicatorPlugin/URLIndicatorPlugin'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
@ -37,6 +38,7 @@ define([
|
|||||||
TimeConductorPlugin,
|
TimeConductorPlugin,
|
||||||
ExampleImagery,
|
ExampleImagery,
|
||||||
ImportExport,
|
ImportExport,
|
||||||
|
SummaryWidget,
|
||||||
URLIndicatorPlugin
|
URLIndicatorPlugin
|
||||||
) {
|
) {
|
||||||
var bundleMap = {
|
var bundleMap = {
|
||||||
@ -123,6 +125,7 @@ define([
|
|||||||
|
|
||||||
plugins.ExampleImagery = ExampleImagery;
|
plugins.ExampleImagery = ExampleImagery;
|
||||||
|
|
||||||
|
plugins.SummaryWidget = SummaryWidget;
|
||||||
plugins.URLIndicatorPlugin = URLIndicatorPlugin;
|
plugins.URLIndicatorPlugin = URLIndicatorPlugin;
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
|
44
src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js
Normal file
44
src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 SummaryWidgetsCompositionPolicy(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
);
|
67
src/plugins/summaryWidget/plugin.js
Executable file
67
src/plugins/summaryWidget/plugin.js
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
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) {
|
||||||
|
return new SummaryWidget(domainObject, openmct);
|
||||||
|
},
|
||||||
|
canView: function (domainObject) {
|
||||||
|
return (domainObject.type === 'summary-widget');
|
||||||
|
},
|
||||||
|
editable: true,
|
||||||
|
key: 'summaryWidgets'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
69
src/plugins/summaryWidget/res/ruleTemplate.html
Normal file
69
src/plugins/summaryWidget/res/ruleTemplate.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</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>
|
188
src/plugins/summaryWidget/src/Condition.js
Normal file
188
src/plugins/summaryWidget/src/Condition.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
438
src/plugins/summaryWidget/src/ConditionEvaluator.js
Normal file
438
src/plugins/summaryWidget/src/ConditionEvaluator.js
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
});
|
373
src/plugins/summaryWidget/src/ConditionManager.js
Normal file
373
src/plugins/summaryWidget/src/ConditionManager.js
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
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);
|
||||||
|
} else {
|
||||||
|
throw event + " is not a supported callback. Supported callbacks are " + this.supportedCallbacks;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('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, {size: 1, strategy: 'latest'}).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;
|
||||||
|
});
|
480
src/plugins/summaryWidget/src/Rule.js
Normal file
480
src/plugins/summaryWidget/src/Rule.js
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
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.thumbnail.css(property, color);
|
||||||
|
self.eventEmitter.emit('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse input text from textbox to prevent HTML Injection
|
||||||
|
* @param {string} msg The text to be Parsed
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function encodeMsg(msg) {
|
||||||
|
return $('<div />').text(msg).html();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = encodeMsg(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) {
|
||||||
|
var text = encodeMsg(elem.value);
|
||||||
|
self.config[inputKey] = text;
|
||||||
|
self.updateDomainObject();
|
||||||
|
if (inputKey === 'name') {
|
||||||
|
self.title.html(text);
|
||||||
|
} else if (inputKey === 'label') {
|
||||||
|
self.thumbnailLabel.html(text);
|
||||||
|
}
|
||||||
|
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.set(self.config.style[inputKey]);
|
||||||
|
onColorInput(self.config.style[inputKey], inputKey);
|
||||||
|
|
||||||
|
input.on('change', function (value) {
|
||||||
|
onColorInput(value, inputKey);
|
||||||
|
self.updateDomainObject();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.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();
|
||||||
|
this.generateDescription();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
self.conditions[0].hideButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.generateDescription();
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Rule;
|
||||||
|
});
|
402
src/plugins/summaryWidget/src/SummaryWidget.js
Normal file
402
src/plugins/summaryWidget/src/SummaryWidget.js
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
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) {
|
||||||
|
this.watchForChangesUnsubscribe = 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();
|
||||||
|
this.watchForChangesUnsubscribe();
|
||||||
|
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();
|
||||||
|
this.addOrRemoveDragIndicator();
|
||||||
|
};
|
||||||
|
|
||||||
|
SummaryWidget.prototype.addOrRemoveDragIndicator = function () {
|
||||||
|
var rules = this.domainObject.configuration.ruleOrder;
|
||||||
|
var rulesById = this.rulesById;
|
||||||
|
|
||||||
|
rules.forEach(function (ruleKey, index, array) {
|
||||||
|
if (array.length > 2 && index > 0) {
|
||||||
|
$('.t-grippy', rulesById[ruleKey].domElement).show();
|
||||||
|
} else {
|
||||||
|
$('.t-grippy', rulesById[ruleKey].domElement).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'Unnamed Rule',
|
||||||
|
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;
|
||||||
|
});
|
336
src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js
Normal file
336
src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
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);
|
||||||
|
//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?']
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
167
src/plugins/summaryWidget/test/SummaryWidgetSpec.js
Normal file
167
src/plugins/summaryWidget/test/SummaryWidgetSpec.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
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 = jasmine.createSpy('observe');
|
||||||
|
mockOpenMCT.objects.observe.andReturn(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 only when in edit mode', function () {
|
||||||
|
summaryWidget.editing = true;
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -79,6 +79,17 @@ define([], function () {
|
|||||||
return this.providers[key];
|
return this.providers[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* A View is used to provide displayable content, and to react to
|
||||||
* associated life cycle events.
|
* associated life cycle events.
|
||||||
|
Loading…
Reference in New Issue
Block a user