Compare commits

..

117 Commits

Author SHA1 Message Date
Charles Hacskaylo
22481fdc31 [Frontend] Set default widget colors and icon
Fixes #1668
2017-09-22 12:05:32 -07:00
Charles Hacskaylo
4a9d27dc79 [Frontend] Hover behaviors refined
Fixes #1668
2017-09-22 11:41:00 -07:00
Charles Hacskaylo
a5a9fefd40 [Frontend] Significant mods to vertical-align for inputs
Fixes #1668
vertical-align: middle for selects, inputs; removed
commented code;
2017-09-22 11:40:06 -07:00
Charles Hacskaylo
dae4074934 [Frontend] Various sanding and shimming
Fixes #1668
Added box-shadow to widget; better styles for
case where controls wrap;
2017-09-22 10:50:40 -07:00
Charles Hacskaylo
a540a3573f [Front-end] WIP Styling for widget in layout
Fixes #1668
2017-09-21 15:30:12 -07:00
Charles Hacskaylo
4e7fe9082c Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 15:26:47 -07:00
Charles Hacskaylo
568141bf81 [Front-end] WIP Styling for widget in layout
Fixes #1668
2017-09-21 15:26:28 -07:00
Deep Tailor
ac3ea43fe5 hide equal to label until select field is available 2017-09-21 15:22:55 -07:00
Charles Hacskaylo
e922e8d504 [Front-end] Add summary widgets to hide-buttons-in-Layout list
Fixes #1668
2017-09-21 15:11:15 -07:00
Charles Hacskaylo
650a877d2a [Front-end] Inject icon directly into widget Label
Fixes #1668
Remove .widget-icon element;
2017-09-21 15:04:35 -07:00
Deep Tailor
1202109c59 Merge branch 'summary-widgets-styling-2' of https://github.com/nasa/openmct into summary-widgets 2017-09-21 14:53:27 -07:00
Deep Tailor
429d7bbd57 add hide/unhide to select.js 2017-09-21 14:53:06 -07:00
Charles Hacskaylo
af749fe71b [Front-end] WIP for widget in Layout
Fixes #1668
Global rename of .l-widget-main to .w-summary-widget:
markup, SCSS and JS; styles; simplify widget markup;
2017-09-21 14:34:11 -07:00
Charles Hacskaylo
cf64c512ce [Front-end] Tweaks
Fixes #1668
More fine-grained selector to display editing UI;
Tweak to "no-data" message content;
2017-09-21 13:48:48 -07:00
Charles Hacskaylo
c5bd3da44a Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 11:39:04 -07:00
Deep Tailor
ff3e49e926 change composition policy to allow summary widgets to be added to folders, but not vice versa. Only allow telemetry producing objects to be added to summary widgets 2017-09-21 11:29:12 -07:00
Charles Hacskaylo
e244a3e431 [Front-end] New null value selection strings
Fixes #1668
2017-09-21 11:23:26 -07:00
Charles Hacskaylo
c5d9fb6fd9 [Front-end] Hide/show "no-data" message element
Fixes #1688
2017-09-21 11:11:12 -07:00
Charles Hacskaylo
4c276ab422 [Front-end] Normalize font sizing in overlay
Fixes #1688
2017-09-21 10:26:47 -07:00
Charles Hacskaylo
bf321abae4 Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 10:09:57 -07:00
Charles Hacskaylo
7336968ef9 [Frontend] Add message markup and styling
Fixes #1688
Add to Summary Widgets UI;
2017-09-20 18:03:03 -07:00
Charles Hacskaylo
d60956948b [Frontend] Sanding to avoid CSS collision
Fixes #1688
2017-09-20 18:02:26 -07:00
Charles Hacskaylo
23d5c2e1ee [Frontend] Refactor messages markup and styling
Fixes #1688
Significant changes! Refactor to allow messages markup and CSS to be used
in Summary Widgets UI
2017-09-20 18:01:26 -07:00
Deep Tailor
2632b8891a Merge latest styling changes from Charles 2017-09-20 14:56:05 -07:00
Deep Tailor
fff4cd9d51 add s-status-no-data class to summary widgets parent div when no telemetry is added and remove when telemetry is added. Add a composition policy to only allow compatible object types to be added to summary widgets 2017-09-20 14:40:31 -07:00
Charles Hacskaylo
be0291cf70 [Frontend] Removed t-condition from testDataItem
Fixes #1668
Fixes #1644
2017-09-19 17:51:16 -07:00
Charles Hacskaylo
b3a6d7271d [Frontend] Mods to main styles
Fixes #1668
Fixes #1644
Added .sm to number inputs; removed padding
from .compact-form label; set inputs and selects
in .compact-form to use $btnStdH for height;
2017-09-19 17:39:12 -07:00
Charles Hacskaylo
ebeed2f236 [Frontend] Significant styling for test data area
Fixes #1668
Fixes #1644
2017-09-19 17:37:15 -07:00
Deep Tailor
337c26c019 fix failing tests 2017-09-18 13:12:23 -07:00
Deep Tailor
730f363f94 Merge branch 'summary-widgets-styling-2' of https://github.com/nasa/openmct into summary-widgets 2017-09-18 09:44:15 -07:00
Deep Tailor
ae30e6110b merge from styling 2017-09-18 09:44:07 -07:00
Charles Hacskaylo
6e4bf3e45b [Frontend] Styling, markup for test data area
Fixes #1668
Fixes #1644
Significant sanding and shimming, WIP;
2017-09-15 18:48:03 -07:00
Charles Hacskaylo
5465ca92f9 [Frontend] Flex layout now working!
Fixes #1668
Fixes #1644
2017-09-15 17:33:28 -07:00
Charles Hacskaylo
e55ea41b0a [Frontend] Align selects
Fixes #1668
Fixes #1644
2017-09-15 17:02:20 -07:00
Charles Hacskaylo
8cfb3cc689 [Frontend] Merge latest master
Fixes #1668
Fixes #1644
2017-09-15 17:01:32 -07:00
Charles Hacskaylo
2d728a1362 [Frontend] WIP Styling continued
Fixes #1668
Markup tweaks
2017-09-15 16:44:11 -07:00
Charles Hacskaylo
99333988df [Frontend] WIP Styling continued
Fixes #1668
Fixes #1644:
VERY WIP! Outer wrapper styling; drag-n-drop
working currently; section-headers;
2017-09-15 16:43:01 -07:00
Deep Tailor
de783d4286 fix circle-ci build errors 2017-09-14 14:11:02 -07:00
Doubek-Kraft
f6c1488ccd [Summary Widgets] Merge with style
Merge remote-tracking branch 'origin/summary-widget-styling-2' into summary-widgets
2017-08-25 14:23:06 -07:00
Doubek-Kraft
26be1ecf37 [Summary Widgets] Merge with style
Merge remote-tracking branch 'origin/summary-widget-styling-2' into summary-widgets

Restore correct glyphs after merge with master
2017-08-25 14:19:50 -07:00
Charles Hacskaylo
38f0f072bb [Merge] Ellipsize added to l-summary-widget
Fixes #1669
2017-08-25 14:19:43 -07:00
Charles Hacskaylo
e5e969665f [Merge] Merge latest master
Fixes #1669
Merge + resolve conflicts;
2017-08-25 14:11:34 -07:00
Doubek-Kraft
ffbb662c99 [Summary Widgets] Unit tests 2017-08-25 14:09:22 -07:00
Doubek-Kraft
bd7b23f896 [Summary Widgets] Merge from style branch
Merge remote-tracking branch 'origin/summary-widget-styling-2' into summary-widgets

Resolve conflicts with divergent rule implementation
2017-08-25 12:43:29 -07:00
Doubek-Kraft
c238def902 [Summary Widgets] Unit tests 2017-08-25 11:57:29 -07:00
Charles Hacskaylo
2d430ece7f [Merge] Thumbs now show icon and label text
Fixes #1669
Updates to allow thumbs to show their associated
icon and label text;
minor CSS tweaks for icon size and ellipsizing;
New rules now don't use first icon by default;
2017-08-25 11:40:46 -07:00
Doubek-Kraft
c92644a661 [Summary Widgets] Merge from master
Merge remote-tracking branch 'origin/master' into summary-widgets

Patch error with SineWave generators after merge
2017-08-25 10:04:32 -07:00
Doubek-Kraft
41ce3c04f7 [Summary Widgets] Unit Tests 2017-08-24 16:52:44 -07:00
Doubek-Kraft
fcf77f359f [Summary Widgets] Unit tests 2017-08-24 12:06:31 -07:00
Doubek-Kraft
40a2737915 [Summary Widgets] Performance Improvements
Remove unecessary re-initialization of Rule objects on certain
user interactions, and cleanup related code
2017-08-24 10:12:55 -07:00
Doubek-Kraft
216489d67f [Summary Widgets] Tests
Add unit tests
2017-08-23 17:01:14 -07:00
Doubek-Kraft
418a393b26 [Sumamry Widgets] Destroy
Implement destroy
2017-08-23 13:09:36 -07:00
Aaron Doubek-Kraft
1f3d744494 [Summary Widgets] Event Emitters
Replace custom event handlers with EventEmitters, remove binds
assoicated with old event handlers, and update documentation and
callback functions
2017-08-22 23:31:45 -07:00
Doubek-Kraft
ff3f2dccba [Summary Widget] Tests
Add and update unit tests

Fix style in markup

Begin implementing destroy
2017-08-22 16:51:00 -07:00
Doubek-Kraft
e69973bd29 [Summary Widgets] Documentation
Finished adding JSDoc comments

Standardize usage of event handler callbacks

Fix rule persistence bug
2017-08-22 11:13:26 -07:00
Doubek-Kraft
05b352cc36 [Summary Widgets] Documentation
Add JSDoc style comments
2017-08-21 17:05:41 -07:00
Doubek-Kraft
9735548999 [Summary Widgets] Add documentation 2017-08-18 16:22:23 -07:00
Doubek-Kraft
f9529b1362 [Summary Widgets] UI for scripted conditions
Add textfield input for JavaScript condition input, and modify the
condition manager and condition evaluator to read JS conditions
correctly from the data model. Currently, custom conditions are
not actually executed and will always be evaluated as false.
2017-08-18 14:27:57 -07:00
Doubek-Kraft
c598cec702 [Summary Widgets] Unit Tests
Add and update tests for select classes

Stub specfiles for test data and DnD
2017-08-17 16:44:33 -07:00
Doubek-Kraft
e9ea1c4a0f [Summary Widgets] Edit Mode
Enable edit mode for summary widgets, and make configuration interface
visible only when the user has entered edit mode

Standardize usage of 'mutate'

Fix collision between widget palettes and other interfaces where
palettes would permanently hide other menus
2017-08-17 13:47:49 -07:00
Doubek-Kraft
e9238ff282 [Summary Widgets] Merge from View API Branch
Merge remote-tracking branch 'origin/view-api-implementation'
 into summary-widgets
2017-08-17 09:31:36 -07:00
Doubek-Kraft
4cebd72cba [Summary Widgets] Merge from style branch
Issues #1644 #1669

Merge remote-tracking branch 'origin/summary-widget-styling-2' into summary-widgets

Integrate new style for inputs
2017-08-16 14:57:29 -07:00
Doubek-Kraft
f8a44d6e71 [Summary Widget] Test Data
Issue #1644

Add user-configurable mock data to test rules. Modify evaluator to
gracefully handle uninitialzed test data points.

Fix DnD bug where drag position was not tracked correctly
2017-08-16 14:37:03 -07:00
Pete Richards
d0745b300b Allow views to be editable 2017-08-16 11:20:04 -07:00
Charles Hacskaylo
2a4e0a3081 [Frontend] Mods to custom selects
Fixes #1669
line-height, context arrow positioning and color
2017-08-16 10:50:08 -07:00
Charles Hacskaylo
1ff19f9574 [Front-end] Refinements to palette .selected
Fixes #1669
Unit tested in espresso and snow themes as well;
2017-08-15 16:04:07 -07:00
Charles Hacskaylo
7ef84cb50d [Front-end] Fix palette menu when no icon is selected
Fixes #1669
2017-08-15 15:51:32 -07:00
Charles Hacskaylo
cd05c70d64 [Front-end] Fixes to palette CSS
Fixes #1669
Changed .selected to avoid icon collision when
applying .selected to a an item in an icon palette;
Moved color defs into theme constants files;
TO-DO: fix custom select alignment issues,
fix menu when no icon is selected,
unit test in Snow theme.
2017-08-15 15:24:18 -07:00
Doubek-Kraft
568473b82f [Summary Widgets] Rule Reorders
Cleanup Widget DnD code and package it as a module
2017-08-14 16:24:38 -07:00
Doubek-Kraft
c61b074755 [Summary Widgets] Rule Reorders
Re-implement drag and drop with a 'sortable list' style interface
2017-08-14 14:21:37 -07:00
Doubek-Kraft
8ed66ab4ab [Summary Widgets] Rule Reorders
Implement drag and drop rule reorders using the native HTML5 API
2017-08-11 16:57:40 -07:00
Doubek-Kraft
b2502dd998 [Summary Widgets] Selection classes for palettes
Merge remote-tracking branch 'origin/summary-widget-styling-2' into
 summary-widgets. Issues #1669 #1644

Update palette classes to apply a visual indicator for selection
to their items.
2017-08-10 10:39:15 -07:00
Doubek-Kraft
856eedbf9d [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.
2017-08-09 16:54:19 -07:00
Charles Hacskaylo
0c0ca6e6af [Frontend] Colors for palette defined
Fixes #1669
2017-08-09 16:00:33 -07:00
Doubek-Kraft
498b797e49 [Summary Widgets] Fix input issue
Fix a bug in the compsosition object selector where it would not display its
currently selected item on a composition add.

Update names of modules to more accurately describe their function.
2017-08-09 10:44:41 -07:00
Doubek-Kraft
02c33388ba [Summary Widgets] Generate Rule Descriptions
Dynamically update the rule description based on the current state
of the rules' conditions
2017-08-08 17:47:54 -07:00
Charles Hacskaylo
8a8e3cc055 [Front-end] WIP colors for Summary Widget palettes
Fixes #1669
2017-08-08 16:11:52 -07:00
Charles Hacskaylo
36d60b16e9 [Front-end] Updated Style Guide
Fixes #1669
Updated palette example in Style Guide to use `no-selection` and `selected` classes;
2017-08-08 15:35:41 -07:00
Charles Hacskaylo
de3114568b [Front-end] Implemented no-selection class in widgets
Fixes #1669
Added `no-selection` class to "None" selection choice in widgets palette;
2017-08-08 15:28:48 -07:00
Doubek-Kraft
eb5835faeb [Summary Widgets] Fix color palettes
Fix color palette bug where the 'none' option was not recognized as
a selectable item

Add ability to toggle 'none' option, and remove it from the text color
control

Remove 'grippy' element when only one user-defined rule exists

Fix format of requirejs headers

Add tooltips
2017-08-08 15:27:46 -07:00
Charles Hacskaylo
ff1ddb0b79 [Front-end] Added .selected class for palette items
Fixes #1669
Added `.selected` in `.s-palette-item` class;
Re-orged styles in palette.scss to place in .l-* and .s-*
classes properly;
2017-08-08 15:16:20 -07:00
Charles Hacskaylo
15b127bb2e [Front-end] Added 'no-selection' CSS class
Fixes #1669
Added to _global.scss;
implemented in color.html;
2017-08-08 15:09:56 -07:00
Doubek-Kraft
e4ed881f6d [Summary Widgets] Code Style
Fix code style issues

Update plugin syntax to more closely match existing plugins
2017-08-08 12:06:20 -07:00
Doubek-Kraft
7b62cf130c [Summary Widget] Add unit tests
Add tests for input classes

Fix code style errors
2017-08-07 17:07:21 -07:00
Doubek-Kraft
72fd2e531c [Summary Widget] Assorted Cleanup
Abstract palette behavior to superclass, and base new color and icon
palette classes on it

Add unit tests

Move private event handler methods out of object prototypes
2017-08-04 16:15:47 -07:00
Doubek-Kraft
4a5392ef78 [Summary Widgets] Minor Fixes
Update zepto in bower to enforce v1.2.0

Fix bug where summary widget conditions would modify appearance of other summary widgets

Move code to plugins directory

Stub for unit tests
2017-08-03 13:09:05 -07:00
Doubek-Kraft
0150a708ca [Evaluation] Implement condition evaluation
Issue #1644

Add telemetry subscription handling, and implement the execution of
rules.

Provide a toggle for 'any', 'all', or 'JavaScript' trigger

Add documentation
2017-08-02 14:31:26 -07:00
Doubek-Kraft
eacc181d5e [Inputs] Add value inputs for rule configuration
Dynamically add value inputs when an operation is being configured.

Cleanup code related to removed label caching feature
2017-08-01 16:03:08 -07:00
Doubek-Kraft
405bb55881 [Refactor] Cleanup old files 2017-08-01 13:51:20 -07:00
Doubek-Kraft
4a35508459 [Refactor] Overhaul of code structure
Re-implement widgets in with a module-oriented structure. Fix issues related to
asychronous loading of telemetry and composition.

Package inputs as re-usable, Zepto-based modules.
2017-08-01 13:40:04 -07:00
Doubek-Kraft
98a9d71a2e [Summary Widgets] Implementation for conditions
Support configuring and persisting multiple conditions per rule
2017-07-26 09:33:27 -07:00
Doubek-Kraft
a1596d0b06 [Summary Widgets] Make conditions persistable
Issue #1644

Add implementation and persistability for a single condition configuration
field. Baseline for adding arbitrary numbers of rules
2017-07-25 14:21:53 -07:00
Doubek-Kraft
4b3be4c483 [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
2017-07-24 12:15:39 -07:00
Doubek-Kraft
0fa8472db1 [Bug Fix] Fix text input handlers
Issue #1644

Fix merge issue related to inputs for label and message
2017-07-24 10:10:28 -07:00
Charles Hacskaylo
e1e2dca1d8 [Frontend] WIP summary widget styling
Fixes #1654
Refactor .l-color-palette to .l-palette, includes
file renaming; add icon-palette in WidgetView.js
2017-07-21 18:07:49 -07:00
Doubek-Kraft
755c013ec8 [Summary Widgets] Merge with style
Issue #1644

Merge and resolve conflicts with style branch

Fix rule indexing bug
2017-07-21 16:19:15 -07:00
Doubek-Kraft
eab702b763 [Summary Widgets] Link markup to implementation
Add additional implemenation for new markup. Rules now store label
and message, and label is applied to widget. Implementation for
duplicate.
2017-07-21 15:59:53 -07:00
Charles Hacskaylo
d15446ac91 [Frontend] WIP summary widget styling
Fixes #1654
Mod .grippy to point at new glyph class;
Stubbed in icon palette control in markup;
2017-07-21 10:32:20 -07:00
Charles Hacskaylo
500733afb2 [Frontend] Add new icon-grippy
Fixes #1654
2017-07-21 10:25:18 -07:00
Charles Hacskaylo
2aa04b0a56 Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling 2017-07-21 10:04:23 -07:00
Doubek-Kraft
c051f342af [Style] Merge with style branch
Merge remote-tracking branch 'origin/summary-widgets-styling' into summary-widgets

Apply new style and wire up new markup for interaction
2017-07-21 09:58:23 -07:00
Charles Hacskaylo
8aeb365f5f [Frontend] WIP summary widget styling
Fixes #1654
Add condition duplicate and delete buttons
2017-07-21 09:56:57 -07:00
Charles Hacskaylo
827a28313d [Frontend] WIP summary widget styling
Fixes #1654
Fixes and tweaks post-merge
2017-07-21 09:48:54 -07:00
Doubek-Kraft
c83de8aad2 [Summary Widgets] Minor UI implementation 2017-07-21 09:24:27 -07:00
Charles Hacskaylo
b55f43b8df [Frontend] WIP summary widget styling
Fixes #1654
Merge latest from summary-widgets branch
2017-07-21 09:15:19 -07:00
Charles Hacskaylo
8466723a90 [Frontend] WIP summary widget styling
Fixes #1654
Minor tweaks; class renaming; restructured
main containers in ruleTemplate.html;
minor updates for class names in WidgetView.js;
2017-07-20 19:01:13 -07:00
Charles Hacskaylo
a103b4dbff [Frontend] WIP summary widget styling
Fixes #1654
Significant mods to markup and SCSS;
2017-07-20 17:56:57 -07:00
Charles Hacskaylo
826ac3a947 [Frontend] WIP summary widget styling
Fixes #1654
Ported .view-control SCSS out of tree context to
allow more independent use;
2017-07-20 14:31:59 -07:00
Charles Hacskaylo
597327f138 [Frontend] WIP summary widget styling
Fixes #1654
New SCSS for widgets; markup mods in progress
2017-07-20 14:09:17 -07:00
Charles Hacskaylo
bef79402ca [Frontend] WIP summary widget styling
Fixes #1654
Ported .inspector-config SCSS from _inspector.scss
and renamed to .l-compact-form;
2017-07-20 14:08:49 -07:00
Doubek-Kraft
e68e0c381f [Summary Widgets] Make Rules Deletable
Add delete functionality and UI

Improve process for indexing and rendering rules to support deletion,
and eventually reordering

Fix bug where default style was passed by reference and overwritten
on style updates
2017-07-19 17:38:17 -07:00
Charles Hacskaylo
c73f7259c2 [Frontend] WIP summary widget styling
Fixes #1654
Markup cleanups
2017-07-19 15:38:44 -07:00
Charles Hacskaylo
4c9235ba10 [Frontend] Apply new summary-widget glyph
Fixes #1654
2017-07-19 15:01:04 -07:00
Charles Hacskaylo
55e2a77df8 [Frontend] Add new summary-widget glyph
Fixes #1654
Updated font files and icomoon json file;
2017-07-19 15:00:46 -07:00
Doubek-Kraft
cfbff02e7f [Summary Widgets] Populate rule config inputs
Add rule configuration inputs, populated with domain objects, metadata,
and appropriate operations for a given type
2017-07-18 16:19:46 -07:00
Doubek-Kraft
54980fb296 [Summary Widgets] Add summary widgets, WIP
Add a summary widget domain object type

Implement basic interface and style configuration for rules
2017-07-18 10:58:55 -07:00
Pete Richards
ba98d9315c [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.
2017-07-05 13:56:32 -07:00
117 changed files with 6567 additions and 1235 deletions

View File

@@ -18,7 +18,7 @@
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"zepto": "^1.1.6",
"zepto": "1.2.0",
"eventemitter3": "^1.2.0",
"lodash": "3.10.1",
"almond": "~0.3.2",

View File

@@ -4,6 +4,12 @@ deployment:
commands:
- npm install canvas nomnoml
- ./build-docs.sh
- git fetch --unshallow
- git push git@heroku.com:openmctweb-demo.git $CIRCLE_SHA1:refs/heads/master
openmct-demo:
branch: live_demo
heroku:
appname: openmct-demo
openmctweb-staging-deux:
branch: mobile
heroku:

View File

@@ -59,7 +59,7 @@ define([
if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request.hasOwnProperty(prop)) {
if (request && request.hasOwnProperty(prop)) {
workerRequest[prop] = request[prop];
}
if (!workerRequest[prop]) {
@@ -78,8 +78,8 @@ define([
return this.workerInterface.request(workerRequest);
};
GeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
GeneratorProvider.prototype.subscribe = function (domainObject, callback, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
return this.workerInterface.subscribe(workerRequest, callback);
};

View File

@@ -127,8 +127,7 @@
{ 'meaning': 'Timer object', 'cssClass': 'icon-timer', 'cssContent': 'e1127', 'htmlEntity': '&#xe1127' },
{ 'meaning': 'Data Topic', 'cssClass': 'icon-topic', 'cssContent': 'e1128', 'htmlEntity': '&#xe1128' },
{ 'meaning': 'Fixed Position object', 'cssClass': 'icon-box-with-dashed-lines', 'cssContent': 'e1129', 'htmlEntity': '&#xe1129' },
{ 'meaning': 'Summary Widget', 'cssClass': 'icon-summary-widget', 'cssContent': 'e1130', 'htmlEntity': '&#xe1130' },
{ 'meaning': 'Notebook object', 'cssClass': 'icon-notebook', 'cssContent': 'e1131', 'htmlEntity': '&#xe1131' }
{ 'meaning': 'Summary Widget', 'cssClass': 'icon-summary-widget', 'cssContent': 'e1130', 'htmlEntity': '&#xe1130' }
];
"></div>

View File

@@ -121,7 +121,7 @@
<h2>Palettes</h2>
<div class="cols cols1-1">
<div class="col">
<p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one.</p>
<p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one. Selected palette choices should utilize the <code>selected</code> CSS class to visualize indicate that state.</p>
<p>Note that while this example uses static markup for illustrative purposes, don't do this - use a front-end framework with repeaters to build the color choices.</p>
</div>
<mct-example><div style="height: 220px" title="Ignore me, I'm just here to provide space for this example.">
@@ -129,9 +129,9 @@
<div class="s-button s-menu-button menu-element t-color-palette icon-paint-bucket" ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
<span class="color-swatch" style="background: rgb(255, 0, 0);"></span>
<div class="menu l-color-palette" ng-show="toggle.isActive()">
<div class="menu l-palette l-color-palette" ng-show="toggle.isActive()">
<div class="l-palette-row l-option-row">
<div class="l-palette-item s-palette-item " ng-click="ngModel[field] = 'transparent'"></div>
<div class="l-palette-item s-palette-item no-selection"></div>
<span class="l-palette-item-label">None</span>
</div>
<div class="l-palette-row">
@@ -147,7 +147,7 @@
<div class="l-palette-item s-palette-item" style="background: rgb(255, 255, 255);"></div>
</div>
<div class="l-palette-row">
<div class="l-palette-item s-palette-item" style="background: rgb(136, 32, 32);"></div>
<div class="l-palette-item s-palette-item selected" style="background: rgb(255, 0, 0);"></div>
<div class="l-palette-item s-palette-item" style="background: rgb(224, 64, 64);"></div>
<div class="l-palette-item s-palette-item" style="background: rgb(240, 160, 72);"></div>
<div class="l-palette-item s-palette-item" style="background: rgb(255, 248, 96);"></div>

View File

@@ -46,22 +46,9 @@ var gulp = require('gulp'),
name: 'bower_components/almond/almond.js',
include: paths.main.replace('.js', ''),
wrap: {
start: (function () {
var buildVariables = {
version: project.version,
timestamp: moment.utc(Date.now()).format(),
revision: fs.existsSync('.git') ? git.long() : 'Unknown',
branch: fs.existsSync('.git') ? git.branch() : 'Unknown'
};
return fs.readFileSync("src/start.frag", 'utf-8')
.replace(/@@(\w+)/g, function (match, key) {
return buildVariables[key];
});;
}()),
startFile: "src/start.frag",
endFile: "src/end.frag"
},
optimize: 'uglify2',
uglify2: { output: { comments: /@preserve/ } },
mainConfigFile: paths.main,
wrapShim: true
},
@@ -71,6 +58,14 @@ var gulp = require('gulp'),
},
sass: {
sourceComments: true
},
replace: {
variables: {
version: project.version,
timestamp: moment.utc(Date.now()).format(),
revision: fs.existsSync('.git') ? git.long() : 'Unknown',
branch: fs.existsSync('.git') ? git.branch() : 'Unknown'
}
}
};
@@ -81,11 +76,16 @@ if (process.env.NODE_ENV === 'development') {
gulp.task('scripts', function () {
var requirejsOptimize = require('gulp-requirejs-optimize');
var replace = require('gulp-replace-task');
var header = require('gulp-header');
var comment = fs.readFileSync('src/about.frag');
return gulp.src(paths.main)
.pipe(sourcemaps.init())
.pipe(requirejsOptimize(options.requirejsOptimize))
.pipe(sourcemaps.write('.'))
.pipe(replace(options.replace))
.pipe(header(comment, options.replace.variables))
.pipe(gulp.dest(paths.dist));
});

View File

@@ -25,8 +25,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title></title>
<script src="bower_components/requirejs/require.js">
</script>
<script src="bower_components/requirejs/require.js"> </script>
<script>
var THIRTY_MINUTES = 30 * 60 * 1000;
@@ -50,7 +49,7 @@
name: "Fixed",
timeSystem: 'utc',
bounds: {
start: Date.now() - 30 * 60 * 1000,
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
}
},
@@ -65,6 +64,7 @@
}
]
}));
openmct.install(openmct.plugins.SummaryWidget());
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
openmct.time.timeSystem('utc');
openmct.start();

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global requirejs,BUILD_CONSTANTS*/
/*global requirejs*/
requirejs.config({
"paths": {
@@ -91,17 +91,12 @@ requirejs.config({
define([
'./platform/framework/src/Main',
'./src/defaultRegistry',
'./src/MCT',
'./src/plugins/buildInfo/plugin'
], function (Main, defaultRegistry, MCT, buildInfo) {
'./src/MCT'
], function (Main, defaultRegistry, MCT) {
var openmct = new MCT();
openmct.legacyRegistry = defaultRegistry;
if (typeof BUILD_CONSTANTS !== 'undefined') {
openmct.install(buildInfo(BUILD_CONSTANTS));
}
openmct.on('start', function () {
return new Main().run(defaultRegistry);
});

View File

@@ -22,10 +22,12 @@
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
"gulp": "^3.9.1",
"gulp-header": "^1.8.8",
"gulp-jscs": "^3.0.2",
"gulp-jshint": "^2.0.0",
"gulp-jshint-html-reporter": "^0.1.3",
"gulp-rename": "^1.2.2",
"gulp-replace-task": "^0.11.0",
"gulp-requirejs-optimize": "^0.3.1",
"gulp-sass": "^2.2.0",
"gulp-sourcemaps": "^1.6.0",

View File

@@ -26,7 +26,6 @@ define([
"./src/InspectorPaneController",
"./src/BrowseObjectController",
"./src/MenuArrowController",
"./src/ObjectHeaderController",
"./src/navigation/NavigationService",
"./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler",
@@ -37,7 +36,6 @@ define([
"text!./res/templates/browse-object.html",
"text!./res/templates/items/grid-item.html",
"text!./res/templates/browse/object-header.html",
"text!./res/templates/browse/object-header-frame.html",
"text!./res/templates/menu-arrow.html",
"text!./res/templates/back-arrow.html",
"text!./res/templates/items/items.html",
@@ -50,7 +48,6 @@ define([
InspectorPaneController,
BrowseObjectController,
MenuArrowController,
ObjectHeaderController,
NavigationService,
NavigateAction,
OrphanNavigationHandler,
@@ -61,7 +58,6 @@ define([
browseObjectTemplate,
gridItemTemplate,
objectHeaderTemplate,
objectHeaderFrameTemplate,
menuArrowTemplate,
backArrowTemplate,
itemsTemplate,
@@ -144,13 +140,6 @@ define([
"$location",
"$attrs"
]
},
{
"key": "ObjectHeaderController",
"implementation": ObjectHeaderController,
"depends": [
"$scope"
]
}
],
"representations": [
@@ -184,13 +173,6 @@ define([
"type"
]
},
{
"key": "object-header-frame",
"template": objectHeaderFrameTemplate,
"uses": [
"type"
]
},
{
"key": "menu-arrow",
"template": menuArrowTemplate,

View File

@@ -1,31 +0,0 @@
<!--
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.
-->
<span class='type-icon flex-elem {{type.getCssClass()}}'></span>
<span class="l-elem-wrapper l-flex-row flex-elem grows" ng-controller="ObjectHeaderController as controller">
<span ng-if="parameters.mode" class='action flex-elem'>{{parameters.mode}}</span>
<span class='title-label flex-elem holder flex-can-shrink s-input-inline'>{{model.name}}</span>
<span class='t-object-alert t-alert-unsynced flex-elem holder' title='This object is not currently displaying real-time data'></span>
<mct-representation
key="'menu-arrow'"
mct-object='domainObject'
class="flex-elem context-available-w"></mct-representation>
</span>

View File

@@ -20,13 +20,9 @@
at runtime from the About dialog for additional information.
-->
<span class='type-icon flex-elem {{type.getCssClass()}}'></span>
<span class="l-elem-wrapper l-flex-row flex-elem grows" ng-controller="ObjectHeaderController as controller">
<span class="l-elem-wrapper l-flex-row flex-elem grows">
<span ng-if="parameters.mode" class='action flex-elem'>{{parameters.mode}}</span>
<span ng-attr-contenteditable="{{ controller.editable ? true : undefined }}"
class='title-label flex-elem holder flex-can-shrink s-input-inline'
ng-click="controller.edit()"
ng-blur="controller.updateName($event)"
ng-keypress="controller.updateName($event)">{{model.name}}</span>
<span class='title-label flex-elem holder flex-can-shrink'>{{model.name}}</span>
<span class='t-object-alert t-alert-unsynced flex-elem holder' title='This object is not currently displaying real-time data'></span>
<mct-representation
key="'menu-arrow'"

View File

@@ -1,92 +0,0 @@
/*****************************************************************************
* 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 () {
/**
* Controller to provide the ability to inline edit an object name.
*
* @constructor
* @memberof platform/commonUI/browse
*/
function ObjectHeaderController($scope) {
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.editable = this.allowEdit();
}
/**
* Updates the object name on blur and enter keypress events.
*
* @param event the mouse event
*/
ObjectHeaderController.prototype.updateName = function (event) {
if (!event || !event.currentTarget) {
return;
}
if (event.type === 'blur') {
this.updateModel(event);
} else if (event.which === 13) {
this.updateModel(event);
event.currentTarget.blur();
window.getSelection().removeAllRanges();
}
};
/**
* Updates the model.
*
* @param event the mouse event
* @param private
*/
ObjectHeaderController.prototype.updateModel = function (event) {
var name = event.currentTarget.textContent.replace(/\n/g, ' ');
if (name.length === 0) {
name = "Unnamed " + this.domainObject.getCapability("type").typeDef.name;
event.currentTarget.textContent = name;
}
if (name !== this.domainObject.getModel().name) {
this.domainObject.getCapability('mutation').mutate(function (model) {
model.name = name;
});
}
};
/**
* Checks if the domain object is editable.
*
* @private
* @return true if object is editable
*/
ObjectHeaderController.prototype.allowEdit = function () {
var type = this.domainObject && this.domainObject.getCapability('type');
return !!(type && type.hasFeature('creation'));
};
return ObjectHeaderController;
}
);

View File

@@ -1,137 +0,0 @@
/*****************************************************************************
* 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(
["../src/ObjectHeaderController"],
function (ObjectHeaderController) {
describe("The object header controller", function () {
var mockScope,
mockDomainObject,
mockCapabilities,
mockMutationCapability,
mockTypeCapability,
mockEvent,
mockCurrentTarget,
model,
controller;
beforeEach(function () {
mockMutationCapability = jasmine.createSpyObj("mutation", ["mutate"]);
mockTypeCapability = jasmine.createSpyObj("type", ["typeDef", "hasFeature"]);
mockTypeCapability.typeDef = { name: ""};
mockTypeCapability.hasFeature.andCallFake(function (feature) {
return feature === 'creation';
});
mockCapabilities = {
mutation: mockMutationCapability,
type: mockTypeCapability
};
model = {
name: "Test name"
};
mockDomainObject = jasmine.createSpyObj("domainObject", ["getCapability", "getModel"]);
mockDomainObject.getModel.andReturn(model);
mockDomainObject.getCapability.andCallFake(function (key) {
return mockCapabilities[key];
});
mockScope = {
domainObject: mockDomainObject
};
mockCurrentTarget = jasmine.createSpyObj("currentTarget", ["blur", "textContent"]);
mockCurrentTarget.blur.andReturn(mockCurrentTarget);
mockEvent = {
which: {},
type: {},
currentTarget: mockCurrentTarget
};
controller = new ObjectHeaderController(mockScope);
});
it("updates the model with new name on blur", function () {
mockEvent.type = "blur";
mockCurrentTarget.textContent = "New name";
controller.updateName(mockEvent);
expect(mockMutationCapability.mutate).toHaveBeenCalled();
});
it("updates the model with a default for blank names", function () {
mockEvent.type = "blur";
mockCurrentTarget.textContent = "";
controller.updateName(mockEvent);
expect(mockCurrentTarget.textContent.length).not.toEqual(0);
expect(mockMutationCapability.mutate).toHaveBeenCalled();
});
it("does not update the model if the same name", function () {
mockEvent.type = "blur";
mockCurrentTarget.textContent = mockDomainObject.getModel().name;
controller.updateName(mockEvent);
expect(mockMutationCapability.mutate).not.toHaveBeenCalled();
});
it("updates the model on enter keypress event only", function () {
mockCurrentTarget.textContent = "New name";
controller.updateName(mockEvent);
expect(mockMutationCapability.mutate).not.toHaveBeenCalled();
mockEvent.which = 13;
controller.updateName(mockEvent);
expect(mockMutationCapability.mutate).toHaveBeenCalledWith(jasmine.any(Function));
mockMutationCapability.mutate.mostRecentCall.args[0](model);
expect(mockDomainObject.getModel().name).toBe("New name");
});
it("blurs the field on enter key press", function () {
mockCurrentTarget.textContent = "New name";
mockEvent.which = 13;
controller.updateName(mockEvent);
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
});
it("allows editting name when object is creatable", function () {
expect(controller.allowEdit()).toBe(true);
});
it("disallows editting name when object is non-creatable", function () {
mockTypeCapability.hasFeature.andReturn(false);
expect(controller.allowEdit()).toBe(false);
});
});
}
);

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div class="abs top-bar">
<div class="title">{{ngModel.title}}</div>
<div class="dialog-title">{{ngModel.title}}</div>
<div class="hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
</div>
<div class='abs editor'>

View File

@@ -1,11 +1,10 @@
<div class="l-message"
ng-class="'message-severity-' + ngModel.severity">
<div class="ui-symbol type-icon message-type"></div>
<div class="message-contents">
<div class="w-message-contents">
<div class="top-bar">
<div class="title">{{ngModel.title}}</div>
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
</div>
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
<div class="message-body">
<div class="message-action">
{{ngModel.actionText}}
@@ -25,8 +24,6 @@
ng-click="ngModel.primaryOption.callback()">
{{ngModel.primaryOption.label}}
</a>
</div>
</div>
</div>

View File

@@ -1,17 +1,17 @@
<mct-container key="overlay" class="t-message-list">
<div class="message-contents">
<div class="abs top-bar">
<div class="title">{{ngModel.dialog.title}}</div>
<mct-container key="overlay">
<div class="t-message-list">
<div class="top-bar">
<div class="dialog-title">{{ngModel.dialog.title}}</div>
<div class="hint">Displaying {{ngModel.dialog.messages.length}} message<span ng-show="ngModel.dialog.messages.length > 1 ||
ngModel.dialog.messages.length == 0">s</span>
</div>
</div>
<div class="abs message-body">
<div class="w-messages">
<mct-include
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
key="'message'" ng-model="msg.model"></mct-include>
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
key="'message'" ng-model="msg.model"></mct-include>
</div>
<div class="abs bottom-bar">
<div class="bottom-bar">
<a ng-repeat="dialogAction in ngModel.dialog.actions"
class="s-button major"
ng-click="dialogAction.action()">

View File

@@ -21,7 +21,7 @@
-->
<mct-container key="overlay">
<div class="abs top-bar">
<div class="title">{{ngModel.dialog.title}}</div>
<div class="dialog-title">{{ngModel.dialog.title}}</div>
<div class="hint">{{ngModel.dialog.hint}}</div>
</div>
<div class='abs editor'>

View File

@@ -23,7 +23,7 @@
<div class="s-menu-button major create-button" ng-click="createController.toggle()">
<span class="title-label">Create</span>
</div>
<div class="menu super-menu l-create-menu" ng-show="createController.isActive()">
<div class="menu super-menu" ng-show="createController.isActive()">
<mct-representation mct-object="domainObject" key="'create-menu'">
</mct-representation>
</div>

View File

@@ -19,8 +19,8 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="w-menu" ng-controller="CreateMenuController">
<div class="col menu-items">
<div class="contents" ng-controller="CreateMenuController">
<div class="pane left menu-items">
<ul>
<li ng-repeat="createAction in createActions" ng-click="createAction.perform()">
<a ng-mouseover="representation.activeMetadata = createAction.getMetadata()"
@@ -31,15 +31,13 @@
</li>
</ul>
</div>
<div class="col menu-item-description">
<div class="pane right menu-item-description">
<div class="desc-area icon {{ representation.activeMetadata.cssClass }}"></div>
<div class="w-title-desc">
<div class="desc-area title">
{{representation.activeMetadata.name}}
</div>
<div class="desc-area description">
{{representation.activeMetadata.description}}
</div>
<div class="desc-area title">
{{representation.activeMetadata.name}}
</div>
<div class="desc-area description">
{{representation.activeMetadata.description}}
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
"metadata": {
"name": "openmct-symbols-16px",
"lastOpened": 0,
"created": 1506973656040
"created": 1505151140023
},
"iconSets": [
{
@@ -899,14 +899,6 @@
"prevSize": 24,
"code": 921904,
"tempChar": ""
},
{
"order": 139,
"id": 117,
"name": "icon-notebook",
"prevSize": 24,
"code": 921905,
"tempChar": ""
}
],
"metadata": {
@@ -3532,29 +3524,6 @@
{}
]
}
},
{
"id": 117,
"paths": [
"M896 110.8c0-79.8-55.4-127.4-123-105.4l-773 250.6h896v-145.2z",
"M896 320h-896v576c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-448c0-70.4-57.6-128-128-128zM832 832h-384v-320h384v320z"
],
"attrs": [
{},
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 16,
"tags": [
"icon-notebook"
],
"colorPermutations": {
"1161751207457516161751": [
{},
{}
]
}
}
],
"colorThemes": [

View File

@@ -118,5 +118,4 @@
<glyph unicode="&#xe1128;" glyph-name="icon-topic" d="M454.36 483.36l86.3 86.3c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c19.328-19.358 42.832-34.541 69.047-44.082l1.313 171.722-57.64 57.64c-34.407 34.33-81.9 55.558-134.35 55.558s-99.943-21.228-134.354-55.562l-86.296-86.297c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-28.674 28.654v-172.14c19.045-7.022 41.040-11.084 63.984-11.084 52.463 0 99.966 21.239 134.379 55.587zM505.64 412.64l-86.3-86.3c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-86.294 86.294c-2 2-4.2 4-6.36 6v-197.36c33.664-30.72 78.65-49.537 128.031-49.537 52.44 0 99.923 21.22 134.333 55.541l86.296 86.296c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c2-2 4.2-4 6.36-6v197.36c-33.664 30.72-78.65 49.537-128.031 49.537-52.44 0-99.923-21.22-134.333-55.541zM832 960h-128v-192h127.66l0.34-0.34v-639.32l-0.34-0.34h-127.66v-192h128c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM320 128h-127.66l-0.34 0.34v639.32l0.34 0.34h127.66v192h-128c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h128v192z" />
<glyph unicode="&#xe1129;" glyph-name="icon-box-with-dashed-lines" d="M0 576h128v-256h-128v256zM128 831.78l0.22 0.22h191.78v128h-192c-70.606-0.215-127.785-57.394-128-127.979v-192.021h128v191.78zM128 64.22v191.78h-128v-192c0.215-70.606 57.394-127.785 127.979-128h192.021v128h-191.78zM384 960h256v-128h-256v128zM896 64.22l-0.22-0.22h-191.78v-128h192c70.606 0.215 127.785 57.394 128 127.979v192.021h-128v-191.78zM896 960h-192v-128h191.78l0.22-0.22v-191.78h128v192c-0.215 70.606-57.394 127.785-127.979 128zM896 576h128v-256h-128v256zM384 64h256v-128h-256v128zM256 704h512v-512h-512v512z" />
<glyph unicode="&#xe1130;" glyph-name="icon-summary-widget" d="M896 960h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM847.8 349.6l-82.6-143.2-189.6 131.6 19.2-230h-165.4l19.2 230-189.6-131.6-82.6 143.2 208.6 98.4-208.8 98.4 82.6 143.2 189.6-131.6-19.2 230h165.4l-19.2-230 189.6 131.6 82.6-143.2-208.6-98.4 208.8-98.4z" />
<glyph unicode="&#xe1131;" glyph-name="icon-notebook" d="M896 849.2c0 79.8-55.4 127.4-123 105.4l-773-250.6h896v145.2zM896 640h-896v-576c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v448c0 70.4-57.6 128-128 128zM832 128h-384v320h384v-320z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -111,9 +111,7 @@ $bubbleMaxW: 300px;
$reqSymbolW: 15px;
$reqSymbolM: $interiorMargin * 2;
$reqSymbolFontSize: 0.75em;
$inputTextPTopBtm: 3px;
$inputTextPLeftRight: 5px;
$inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
$inputTextP: 3px 5px;
/*************** Wait Spinner Defaults */
$waitSpinnerD: 32px;
$waitSpinnerTreeD: 20px;

View File

@@ -180,6 +180,20 @@ a.disabled {
@include ellipsize();
}
.no-selection {
// aka selection = "None". Used in palettes and their menu buttons.
$c: red; $s: 48%; $e: 52%;
@include background-image(linear-gradient(-45deg,
transparent $s - 5%,
$c $s,
$c $e,
transparent $e + 5%
));
background-repeat: no-repeat;
background-size: contain;
}
.scrolling,
.scroll {
overflow: auto;

View File

@@ -146,7 +146,6 @@ $glyph-icon-timer: '\e1127';
$glyph-icon-topic: '\e1128';
$glyph-icon-box-with-dashed-lines: '\e1129';
$glyph-icon-summary-widget: '\e1130';
$glyph-icon-notebook: '\e1131';
/************************** 16 PX CLASSES */
@@ -261,7 +260,6 @@ $glyph-icon-notebook: '\e1131';
.icon-topic { @include glyphBefore($glyph-icon-topic); }
.icon-box-with-dashed-lines { @include glyphBefore($glyph-icon-box-with-dashed-lines); }
.icon-summary-widget { @include glyphBefore($glyph-icon-summary-widget); }
.icon-notebook { @include glyphBefore($glyph-icon-notebook); }
/************************** 12 PX CLASSES */
.icon-crosshair-12px { @include glyphBefore($glyph-icon-crosshair,'symbolsfont-12px'); }

View File

@@ -26,6 +26,5 @@
display: block;
height: 100%;
width: 100%;
border: none;
}
}

View File

@@ -37,7 +37,7 @@
/********************************* CONTROLS */
@import "controls/breadcrumb";
@import "controls/buttons";
@import "controls/color-palette";
@import "controls/palette";
@import "controls/controls";
@import "controls/lists";
@import "controls/menus";
@@ -80,3 +80,4 @@
@import "autoflow";
@import "features/imagery";
@import "features/time-display";
@import "widgets";

View File

@@ -316,28 +316,23 @@
text-shadow: $shdwItemText;
}
@mixin input-base() {
@mixin input-base($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.6) 0 1px 3px) {
@include appearance(none);
border-radius: $controlCr;
box-sizing: border-box;
&:focus { outline: 0; }
box-shadow: inset $shdw;
background: $bg;
border: none;
color: $fg;
outline: none;
&.error {
background-color: $colorFormFieldErrorBg;
color: $colorFormFieldErrorFg;
}
}
@mixin s-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.6) 0 1px 3px) {
@include input-base();
background: $bg;
box-shadow: inset $shdw;
color: $fg;
}
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.6) 0 1px 3px) {
@include s-input($bg, $fg, $shdw);
border: none;
outline: none;
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg) {
@include input-base($bg, $fg);
}
@mixin contextArrow() {
@@ -349,7 +344,7 @@
}
@mixin nice-textarea($bg: $colorBodyBg, $fg: $colorBodyFg) {
@include nice-input($bg, $fg);
@include input-base($bg, $fg);
padding: $interiorMargin;
}

View File

@@ -0,0 +1,266 @@
/*****************************************************************************
* 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;
padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: pointer;
pointer-events: none;
&[href] {
pointer-events: inherit;
}
}
.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-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;
&.expanded {
min-height: 50px;
height: auto;
opacity: 1;
pointer-events: inherit;
}
}
.widget-rule-content.expanded {
overflow: visible !important;
}
.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
margin: 30px auto;
}
.widget-edit-holder {
display: flex; // Overrides display: none during Browse mode
}
&.s-status-no-data {
.widget-edit-holder {
opacity: 0.3;
pointer-events: none;
}
.t-message-widget-no-data {
display: flex;
}
}
.l-compact-form {
// Overrides on .l-compact-form
ul {
&:last-child { margin: 0; }
li {
@include align-items(flex-start);
@include flex-wrap(nowrap);
line-height: 230%; // Provide enough space when controls wrap
padding: 2px 0;
&:not(.widget-rule-header) {
&:not(.connects-to-previous) {
border-top: 1px solid $colorFormLines;
}
}
&.connects-to-previous {
padding: $interiorMargin 0;
}
> label {
display: block; // Needed to align text to right
text-align: right;
}
}
}
&.s-widget-test-data-item {
// Single line of ul li label span, etc.
ul {
li {
border: none !important;
> label {
display: inline-block;
width: auto;
text-align: left;
}
}
}
}
}
}
.widget-edit-holder {
font-size: 0.8rem;
}
.widget-rules-wrapper {
// Wrapper area that holds n rules
box-sizing: border-box;
overflow-y: scroll;
padding-right: $interiorMargin;
}
.l-widget-rule,
.l-widget-test-data-item {
box-sizing: border-box;
margin-bottom: $interiorMarginSm;
padding: $interiorMargin $interiorMarginLg;
}
.l-widget-thumb-wrapper {
@extend .l-flex-row;
@include align-items(center);
> span { display: block; }
.grippy-holder,
.view-control {
margin-right: $interiorMargin;
width: 1em;
height: 1em;
}
.widget-thumb {
@include flex(1 1 auto);
width: 100%;
}
}
.rule-title {
@include flex(0 1 auto);
color: pullForward($colorBodyFg, 50%);
}
.rule-description {
@include flex(1 1 auto);
@include ellipsize();
color: pushBack($colorBodyFg, 20%);
}
.s-widget-rule,
.s-widget-test-data-item {
background-color: rgba($colorBodyFg, 0.1);
border-radius: $basicCr;
}
.widget-thumb {
@include ellipsize();
@extend .s-summary-widget;
@extend .l-summary-widget;
padding: $interiorMarginSm $interiorMargin;
}
// Hide and show elements in the rule-header on hover
.l-widget-rule,
.l-widget-test-data-item {
.grippy,
.l-rule-action-buttons-wrapper,
.l-condition-action-buttons-wrapper,
.l-widget-test-data-item-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 500ms);
opacity: 0;
}
&:hover {
.grippy,
.l-rule-action-buttons-wrapper,
.l-widget-test-data-item-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 0);
opacity: 1;
}
}
.t-condition {
&:hover {
.l-condition-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 0);
opacity: 1;
}
}
}
}

View File

@@ -234,16 +234,12 @@ textarea {
}
/******************************************************** INPUTS */
%input-base {
@include input-base();
}
input[type="text"],
input[type="search"],
input[type="number"] {
@include nice-input();
vertical-align: baseline;
padding: $inputTextPTopBtm $inputTextPLeftRight;
padding: $inputTextP;
&.numeric {
text-align: right;
}
@@ -261,7 +257,7 @@ input[type="number"] {
input[type="text"].lg { width: 100% !important; }
.l-input-med input[type="text"],
input[type="text"].med { width: 200px !important; }
input[type="text"].sm { width: 50px !important; }
input[type="text"].sm, input[type="number"].sm { width: 50px !important; }
.l-numeric input[type="text"],
input[type="text"].numeric { text-align: right; }
@@ -287,48 +283,20 @@ textarea.lg { position: relative; height: 300px; }
}
}
*[contenteditable].s-input-inline,
input[type="text"].s-input-inline,
.s-input-inline input[type="text"] {
// A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus
@extend %input-base;
@include trans-prop-nice((padding, box-shadow), 250ms);
background: none;
box-shadow: none;
border: 1px solid transparent;
min-width: 20px;
padding-left: 0;
padding-right: 0;
&:hover,
&:focus {
padding-left: $inputTextPLeftRight;
padding-right: $inputTextPLeftRight;
}
&:hover {
border-color: rgba($colorBodyFg, 0.2);
}
&:focus {
@include s-input();
border-color: transparent;
}
}
/******************************************************** SELECTS */
.select {
@include btnSubtle($bg: $colorSelectBg);
@extend .icon-arrow-down; // Context arrow
@if $shdwBtns != none {
margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers
}
//@if $shdwBtns != none { margin: 0 0 2px 0; } // Needed to avoid dropshadow from being clipped by parent containers
display: inline-block;
padding: 0 $interiorMargin;
overflow: hidden;
position: relative;
line-height: $formInputH;
//height: $btnStdH;
//line-height: $btnStdH;
select {
@include appearance(none);
box-sizing: border-box;
&:focus { outline: 0; }
background: none;
color: $colorSelectFg;
cursor: pointer;
@@ -340,11 +308,13 @@ input[type="text"].s-input-inline,
}
}
&:before {
pointer-events: none;
@include transform(translateY(-50%));
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
display: block;
pointer-events: none;
position: absolute;
right: $interiorMargin; top: 0;
right: $interiorMargin;
top: 50%;
}
}
@@ -736,6 +706,30 @@ textarea {
}
}
.view-switcher,
.t-btn-view-large {
@include trans-prop-nice-fade($controlFadeMs);
}
.view-control {
@extend .icon-arrow-right;
cursor: pointer;
font-size: 0.75em;
&:before {
position: absolute;
@include trans-prop-nice(transform, 100ms);
@include transform-origin(center);
}
&.expanded:before {
@include transform(rotate(90deg));
}
}
.grippy {
@extend .icon-grippy;
cursor: move;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {
@@ -753,15 +747,11 @@ body.desktop {
}
.overlay ::-webkit-scrollbar-thumb {
$lr: 15%;
background: $scrollbarThumbColorOverlay;
&:hover { background: $scrollbarThumbColorOverlayHov; }
}
.menu ::-webkit-scrollbar-thumb {
background: $scrollbarThumbColorMenu;
&:hover { background: $scrollbarThumbColorMenuHov; }
}
::-webkit-scrollbar-corner {
background: $scrollbarTrackColorBg;
}

View File

@@ -21,87 +21,97 @@
*****************************************************************************/
/******************************************************** MENU BUTTONS */
.s-menu-button {
// Formerly .btn-menu
@extend .s-button;
span.l-click-area {
// In markup, this element should not enclose anything.
@extend .abs;
}
// Formerly .btn-menu
@extend .s-button;
span.l-click-area {
// In markup, this element should not enclose anything.
@extend .abs;
}
.icon {
font-size: 16px; //120%;
}
.icon {
font-size: 16px; //120%;
}
.title-label {
margin-left: $interiorMarginSm;
}
.title-label {
margin-left: $interiorMarginSm;
}
.icon-swatch,
.color-swatch {
// Used in color menu buttons in toolbar
$d: 10px;
display: inline-block;
border: 1px solid rgba($colorBtnFg, 0.2);
height: $d;
width: $d;
height: $d; width: $d;
line-height: $d;
vertical-align: middle;
margin-left: $interiorMarginSm;
margin-top: -2px;
}
&:after {
// Adds the downward facing 'context available / invoke menu' arrow element
@include contextArrow();
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
}
&.create-button {
@extend .icon-plus;
.title-label {
font-size: 1rem;
&:not(.no-selection) {
border-color: transparent;
}
}
.menu {
left: 0;
text-align: left;
}
&:after {
// Adds the downward facing 'context available / invoke menu' arrow element
@include contextArrow();
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
}
&.create-button {
@extend .icon-plus;
.title-label {
font-size: 1rem;
}
}
.menu {
left: 0;
text-align: left;
}
}
/******************************************************** MENUS THEMSELVES */
.menu-element {
cursor: pointer;
position: relative;
cursor: pointer;
position: relative;
}
.s-menu {
border-radius: $basicCr;
@include containerSubtle($colorMenuBg, $colorMenuFg);
@include boxShdw($shdwMenu);
@include txtShdw($shdwMenuText);
padding: $interiorMarginSm 0;
}
.menu {
border-radius: $basicCr;
@include containerSubtle($colorMenuBg, $colorMenuFg);
@include boxShdw($shdwMenu);
@include txtShdw($shdwMenuText);
padding: $interiorMarginSm 0;
display: block;
position: absolute;
z-index: 10;
ul {
@include menuUlReset();
li {
box-sizing: border-box;
border-top: 1px solid pullForward($colorMenuBg, 10%);
// TODO: reduce size of icons
@extend .s-menu;
display: block;
position: absolute;
z-index: 10;
ul {
@include menuUlReset();
li {
box-sizing: border-box;
border-top: 1px solid pullForward($colorMenuBg, 10%);
color: $colorMenuFg;
line-height: $menuLineH;
padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
position: relative;
white-space: nowrap;
&:first-child {
border: none;
}
&:hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
//color: pullForward($colorMenuBg, 60%);
line-height: $menuLineH;
padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
position: relative;
white-space: nowrap;
&:first-child {
border: none;
}
&:hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
&:before {
color: $colorMenuHovIc;
}
}
}
&:before {
@extend .ui-symbol;
@extend .type-icon;
@@ -109,8 +119,8 @@
display: inline-block;
left: $interiorMargin * 2;
}
}
}
}
}
}
.menu,
@@ -118,97 +128,94 @@
.context-menu,
.super-menu,
.s-menu-button .menu {
pointer-events: auto;
ul li {
a.menu-item-a {
pointer-events: auto;
ul li {
a.menu-item-a {
color: $colorMenuFg;
display: block;
}
}
&:before,
a.menu-item-a:before {
color: $colorMenuIc;
left: $interiorMargin;
}
}
}
}
.checkbox-menu {
// Used in search dropdown in tree
@extend .context-menu;
ul li {
padding-left: 50px;
.checkbox {
$d: 0.7rem;
position: absolute;
left: $interiorMargin;
top: ($menuLineH - $d) / 1.5;
em {
height: $d;
width: $d;
&:before {
font-size: 7px !important;
height: $d;
width: $d;
line-height: $d;
}
}
}
&:before {
// Used in search dropdown in tree
@extend .context-menu;
ul li {
padding-left: 50px;
.checkbox {
$d: 0.7rem;
position: absolute;
left: $interiorMargin;
top: ($menuLineH - $d) / 1.5;
em {
height: $d;
width: $d;
&:before {
font-size: 7px !important;
height: $d;
width: $d;
line-height: $d;
}
}
}
&:before {
// Type icon
left: 25px;
}
}
}
.super-menu,
.super-menu > mct-representation,
.super-menu > .contents {
box-sizing: border-box;
display: block;
position: relative;
left: 25px;
}
}
}
.super-menu {
$plw: 50%;
$prw: 100% - $plw;
position: absolute;
.w-menu {
align-items: stretch;
display: flex;
flex-direction: row;
margin: $interiorMarginLg;
}
.col {
box-sizing: border-box;
flex: 1 1 auto;
overflow-x: hidden;
&.menu-items {
border-right: 1px solid pullForward($colorMenuBg, 10%);
overflow-y: auto;
padding-right: $interiorMargin;
width: $plw;
ul {
li {
border-radius: $controlCr;
padding-left: 30px;
border-top: none;
}
}
}
&.menu-item-description {
$p: $interiorMargin * 3;
overflow-y: hidden;
padding: $p $p 0 $p;
width: $prw;
$w: 500px;
$h: $w - 20;
$plw: 50%;
$prw: 50%;
display: block;
width: $w;
height: $h;
.contents {
@include absPosDefault($interiorMargin);
}
.pane {
box-sizing: border-box;
&.menu-items {
border-right: 1px solid pullForward($colorMenuBg, 10%);
left: 0;
padding-right: $interiorMargin;
right: auto;
width: $plw;
overflow-x: hidden;
overflow-y: auto;
ul {
li {
border-radius: $controlCr;
padding-left: 30px;
border-top: none;
}
}
}
&.menu-item-description {
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
.desc-area {
&.icon {
color: $colorCreateMenuLgIcon;
font-size: 8em;
margin-bottom: $interiorMargin * 3;
position: relative;
text-align: center;
}
&.title {
color: $colorCreateMenuText;
font-size: 1.2em;
margin-bottom: $interiorMargin * 2;
}
&.description {
color: pushBack($colorCreateMenuText, 20%);
@@ -216,104 +223,67 @@
line-height: 1.5em;
}
}
}
}
.w-title-desc {
display: flex;
flex-direction: column;
overflow: hidden; // Height set in specific menu instances
}
// Specific menu instances
&.l-create-menu {
width: 500px;
.col {
max-height: 70vh;
}
.w-title-desc {
height: 190px;
}
.desc-area {
&.icon {
font-size: 8em;
height: 135px;
margin-bottom: $interiorMargin * 3;
}
&.title {
font-size: 1.2em;
margin-bottom: $interiorMargin * 2;
}
}
}
}
}
&.mini {
width: 400px;
.col {
max-height: 50vh;
height: 300px;
.pane {
&.menu-items {
font-size: 0.8em;
}
&.menu-item-description {
$p: $interiorMargin * 2;
padding: $p $p 0 $p;
}
}
.w-title-desc {
height: 180px;
}
.desc-area {
&.icon {
font-size: 4em;
height: 70px;
margin-bottom: $interiorMargin * 3;
}
&.title {
font-size: 1em;
margin-bottom: $interiorMargin * 2;
padding: $interiorMargin * 3;
.desc-area {
&.icon {
font-size: 4em;
}
&.title {
font-size: 1em;
}
}
}
}
}
}
.context-menu {
font-size: 0.80rem;
font-size: 0.80rem;
}
.context-menu-holder,
.menu-holder {
position: absolute;
z-index: 120;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
}
&.go-left .context-menu,
&.go-left .menu {
right: 0;
}
&.go-up .context-menu,
&.go-up .menu {
bottom: 0;
}
position: absolute;
z-index: 120;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
}
&.go-left .context-menu,
&.go-left .menu {
right: 0;
}
&.go-up .context-menu,
&.go-up .menu {
bottom: 0;
}
}
.context-menu-holder {
pointer-events: none;
height: 200px;
width: 170px;
pointer-events: none;
height: 200px;
width: 170px;
}
.btn-bar.right .menu,
.menus-to-left .menu {
z-index: 79;
left: auto;
right: 0;
width: auto;
left: auto;
right: 0;
width: auto;
}
.menus-up .menu {
bottom: $btnStdH;
top: auto;
bottom: $btnStdH; top: auto;
}

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/******************************************************************* STATUS BLOCK ELEMS */
@mixin statusBannerColors($bg, $fg: $colorStatusFg) {
$bgPb: 30%;
$bgPbD: 10%;
@@ -136,7 +136,7 @@
}
}
/* Styles for messages and message banners */
/******************************************************************* MESSAGE BANNERS */
.message {
&.block {
border-radius: $basicCr;
@@ -236,132 +236,150 @@
}
}
@mixin messageBlock($iconW: 32px) {
.type-icon.message-type {
/******************************************************************* MESSAGES */
/* Contexts:
In .t-message-list
In .overlay as a singleton
Inline in the view area
*/
// Archetypal message
.l-message {
$iconW: 32px;
//@include test();
@include display(flex);
@include flex-direction(row);
@include align-items(stretch);
padding: $interiorMarginLg;
&:before {
// Icon
@include flex(0 1 auto);
@include txtShdw($shdwStatusIc);
@extend .icon-bell;
color: $colorStatusDefault;
font-size: $iconW;
padding: 1px;
width: $iconW + 2;
margin-right: $interiorMarginLg;
}
.message-severity-info .type-icon.message-type {
&.message-severity-info:before {
@extend .icon-info;
color: $colorInfo;
}
.message-severity-alert .type-icon.message-type {
@extend .icon-bell;
&.message-severity-alert:before {
color: $colorWarningLo;
}
.message-severity-error .type-icon.message-type {
&.message-severity-error:before {
@extend .icon-alert-rect;
color: $colorWarningHi;
}
}
/* Paths:
t-dialog | t-dialog-sm > t-message-single | t-message-list > overlay > holder > contents > l-message >
message-type > (icon)
message-contents >
top-bar >
title
hint
editor >
(if displaying list of messages)
ul > li > l-message >
... same as above
bottom-bar
*/
.l-message {
.w-message-contents {
@include flex(1 1 auto);
@include display(flex);
@include flex-direction(row);
@include align-items(stretch);
.type-icon.message-type {
@include flex(0 1 auto);
position: relative;
}
.message-contents {
@include flex(1 1 auto);
margin-left: $overlayMargin;
position: relative;
@include flex-direction(column);
.top-bar,
> div,
> span {
//@include test(red);
margin-bottom: $interiorMargin;
}
.message-body {
//@include test(blue);
@include flex(1 1 100%);
}
}
// Singleton in an overlay dialog
.t-message-single .l-message,
.t-message-single.l-message {
$iconW: 80px;
@include absPosDefault();
padding: 0;
&:before {
font-size: $iconW;
width: $iconW + 2;
}
.title {
font-size: 1.2em;
}
}
// Singleton inline in a view
.t-message-inline .l-message,
.t-message-inline.l-message {
border-radius: $controlCr;
&.message-severity-info { background-color: rgba($colorInfo, 0.3); }
&.message-severity-alert { background-color: rgba($colorWarningLo, 0.3); }
&.message-severity-error { background-color: rgba($colorWarningHi, 0.3); }
.w-message-contents.l-message-body-only {
.message-body {
margin-bottom: $interiorMarginLg * 2;
margin-top: $interiorMargin;
}
}
}
// In a list
.t-message-list {
@include absPosDefault();
@include display(flex);
@include flex-direction(column);
// Message as singleton
.t-message-single {
@include messageBlock(80px);
}
body.desktop .t-message-single {
.l-message,
.bottom-bar {
@include absPosDefault();
> div,
> span {
//@include test(red);
margin-bottom: $interiorMargin;
}
.bottom-bar {
top: auto;
height: $ovrFooterH;
.w-messages {
@include flex(1 1 100%);
overflow-y: auto;
padding-right: $interiorMargin;
}
// Each message
.l-message {
border-radius: $controlCr;
background: rgba($colorOvrFg, 0.1);
margin-bottom: $interiorMargin;
.hint,
.bottom-bar {
text-align: left;
}
}
}
@include phonePortrait {
.t-message-single {
.l-message {
@include flex-direction(column);
.message-contents { margin-left: 0; }
}
.type-icon.message-type {
.t-message-single .l-message,
.t-message-single.l-message {
@include flex-direction(column);
&:before {
margin-right: 0;
margin-bottom: $interiorMarginLg;
width: 100%;
text-align: center;
width: 100%;
}
.bottom-bar {
text-align: center !important;
}
}
}
// Messages in list
.t-message-list {
@include messageBlock(32px);
.message-contents {
.l-message {
border-radius: $controlCr;
background: rgba($colorOvrFg, 0.1);
margin-bottom: $interiorMargin;
padding: $interiorMarginLg;
.message-contents,
.bottom-bar {
position: relative;
}
.message-contents {
font-size: 0.9em;
margin-left: $interiorMarginLg;
.message-action { color: pushBack($colorOvrFg, 20%); }
.bottom-bar { text-align: left; }
}
.top-bar,
.message-body {
margin-bottom: $interiorMarginLg;
text-align: center;
.s-button {
display: block;
width: 100%;
}
}
}
}
body.desktop .t-message-list {
.message-contents .l-message { margin-right: $interiorMarginLg; }
.w-message-contents { padding-right: $interiorMargin; }
}
// Alert elements in views

View File

@@ -19,11 +19,10 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.l-color-palette {
.l-palette {
$d: 16px;
$colorsPerRow: 10;
$m: 1;
$colorSelectedColor: #fff;
box-sizing: border-box;
padding: $interiorMargin !important;
@@ -33,46 +32,41 @@
line-height: $d;
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
&.l-option-row {
margin-bottom: $interiorMargin;
.s-palette-item {
border-color: $colorPaletteFg;
}
}
.l-palette-item {
box-sizing: border-box;
@include txtShdwSubtle(0.8);
@include trans-prop-nice-fade(0.25s);
border: 1px solid transparent;
color: $colorSelectedColor;
display: block;
float: left;
height: $d; width: $d;
line-height: $d * 0.9;
margin: 0 ($m * 1px) ($m * 1px) 0;
position: relative;
text-align: center;
&:before {
// Check mark for selected items
font-size: 0.8em;
}
}
.s-palette-item {
border: 1px solid transparent;
color: $colorPaletteFg;
text-shadow: $shdwPaletteFg;
@include trans-prop-nice-fade(0.25s);
&:hover {
@include trans-prop-nice-fade(0);
border-color: $colorSelectedColor !important;
border-color: $colorPaletteSelected !important;
}
&.selected {
border-color: $colorPaletteSelected;
box-shadow: $shdwPaletteSelected; //Needed to see selection rect on light colored swatches
}
}
.l-palette-item-label {
margin-left: $interiorMargin;
}
&.l-option-row {
margin-bottom: $interiorMargin;
.s-palette-item {
border-color: $colorBodyFg;
}
}
}
}
}

View File

@@ -20,7 +20,19 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.section-header {
border-radius: $basicCr;
background: $colorFormSectionHeader;
color: lighten($colorBodyFg, 20%);
font-size: inherit;
margin: $interiorMargin 0;
padding: $formTBPad $formLRPad;
text-transform: uppercase;
.view-control {
display: inline-block;
margin-right: $interiorMargin;
width: 1em;
height: 1em;
}
}
.form {
@@ -41,15 +53,6 @@
}
}
.section-header {
border-radius: $basicCr;
background: $colorFormSectionHeader;
$c: lighten($colorBodyFg, 20%);
color: $c;
font-size: 0.8em;
padding: $formTBPad $formLRPad;
}
.form-row {
$m: $interiorMargin;
box-sizing: border-box;
@@ -57,9 +60,6 @@
margin-bottom: $interiorMarginLg * 2;
padding: $formTBPad 0;
position: relative;
//&ng-form {
// display: block;
//}
&.first {
border-top: none;
@@ -171,3 +171,106 @@
padding: $interiorMargin;
}
}
/**************************************************************************** COMPACT FORM */
// ul > li > label, control
// Make a new UL for each form section
// Allow control-first, controls-below
// TO-DO: migrate work in branch ch-plot-styling that users .inspector-config to use classes below instead
.l-compact-form .tree ul li,
.l-compact-form ul li {
padding: 2px 0;
}
.l-compact-form {
$labelW: 40%;
$minW: $labelW;
ul {
margin-bottom: $interiorMarginLg;
li {
@include display(flex);
@include flex-wrap(wrap);
@include align-items(center);
label,
.control {
@include display(flex);
}
label {
line-height: inherit;
width: $labelW;
}
.controls {
@include flex-grow(1);
margin-left: $interiorMargin;
input[type="text"],
input[type="search"],
input[type="number"],
.select {
height: $btnStdH;
line-height: $btnStdH;
vertical-align: middle;
}
.e-control {
// Individual form controls
&:not(:first-child) {
margin-left: $interiorMarginSm;
}
}
}
&.connects-to-previous {
padding-top: 0;
}
&.section-header {
margin-top: $interiorMarginLg;
border-top: 1px solid $colorFormLines;
}
&.controls-first {
.control {
@include flex-grow(0);
margin-right: $interiorMargin;
min-width: 0;
order: 1;
width: auto;
}
label {
@include flex-grow(1);
order: 2;
width: auto;
}
}
&.controls-under {
display: block;
.control, label {
display: block;
width: auto;
}
ul li {
border-top: none !important;
padding: 0;
}
}
}
}
.form-error {
// Block element that visually flags an error and contains a message
background-color: $colorFormFieldErrorBg;
color: $colorFormFieldErrorFg;
border-radius: $basicCr;
display: block;
padding: 1px 6px;
&:before {
content: $glyph-icon-alert-triangle;
display: inline;
font-family: symbolsfont;
margin-right: $interiorMarginSm;
}
}
}

View File

@@ -129,6 +129,9 @@
}
.s-filter {
input[type="search"] {
@include input-base();
}
.clear-icon,
.menu-icon,
&:before {

View File

@@ -79,6 +79,7 @@
// Dialog boxes, size constrained and centered in desktop/tablet
&.l-dialog {
font-size: 0.8rem;
.s-button {
&:not(.major) {
@include btnSubtle($bg: $colorOvrBtnBg, $bgHov: pullForward($colorOvrBtnBg, 10%), $fg: $colorOvrBtnFg, $fgHov: $colorOvrBtnFg, $ic: $colorOvrBtnFg, $icHov: $colorOvrBtnFg);
@@ -125,9 +126,9 @@
@include containerSubtle($colorOvrBg, $colorOvrFg);
}
.title {
.dialog-title {
@include ellipsize();
font-size: 1.2em;
font-size: 1.5em;
line-height: 120%;
margin-bottom: $interiorMargin;
}
@@ -156,8 +157,6 @@
left: 0;
right: 0;
overflow: auto;
padding-right: $interiorMargin;
padding-bottom: $interiorMargin;
.field.l-input-med {
input[type='text'] {
width: 100%;

View File

@@ -52,21 +52,13 @@ ul.tree {
.view-control {
color: $colorItemTreeVC;
font-size: 0.75em;
margin-right: $interiorMargin;
height: 100%;
line-height: inherit;
width: $treeVCW;
&:before { display: none; }
&.has-children {
&:before {
position: absolute;
@include trans-prop-nice(transform, 100ms);
content: "\e904";
@include transform-origin(center);
}
&.expanded:before {
@include transform(rotate(90deg));
}
&:before { display: block; }
}
}

View File

@@ -44,7 +44,8 @@
&.t-object-type-timer,
&.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
// Note that this will hide the view Switcher button if applied
// to an object that has it.
@@ -120,19 +121,22 @@
}
}
&.t-frame-outer .s-input-inline {
// Prevent inline inputs from being edited when nested in a Layout
pointer-events: none !important;
}
/********************************************************** OBJECT TYPES */
.t-object-type-hyperlink {
/********************************************************** OBJECT TYPES */
.t-object-type-hyperlink,
.t-object-type-summary-widget {
.object-holder {
overflow: hidden;
}
.w-summary-widget,
.l-summary-widget,
.l-hyperlink.s-button {
@extend .abs;
}
.l-summary-widget,
.l-hyperlink.s-button {
// When a hyperlink is a button in a frame, make it expand to fill out to the object-holder
@extend .abs;
//@extend .abs;
.label {
@include ellipsize();
@include transform(translateY(-50%));

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<!-- look at action-button for example -->
<span class="t-filter l-filter"
<span class="t-filter l-filter s-filter"
ng-controller="GetterSetterController">
<input type="search"
class="t-filter-input"

View File

@@ -108,11 +108,8 @@ define(
getMetadata();
});
var mutation = $scope.ngModel.selectedObject.getCapability('mutation');
var unlisten = mutation.listen(getMetadata);
$scope.$on('$destroy', unlisten);
}
return ObjectInspectorController;
}
);

View File

@@ -39,18 +39,10 @@ define(
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
["$watch", "$on"]
["$watch"]
);
mockScope.ngModel = {};
mockScope.ngModel.selectedObject = {
getCapability: function () {
return {
listen: function () {
return true;
}
};
}
};
mockScope.ngModel.selectedObject = 'mock selected object';
mockObjectService = jasmine.createSpyObj(
"objectService",

View File

@@ -201,15 +201,13 @@ $shdwItemTreeIcon: 0.6;
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 7px;
$scrollbarTrackShdw: rgba(#000, 0.5) 0 1px 5px;
$scrollbarTrackColorBg: transparent; //rgba(#000, 0.4);
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.7) 0 1px 5px;
$scrollbarTrackColorBg: rgba(#000, 0.4);
$scrollbarThumbColor: pullForward($colorBodyBg, 10%);
$scrollbarThumbColorHov: pullForward($scrollbarThumbColor, 2%);
$scrollbarThumbColorOverlay: pullForward($colorOvrBg, 10%);
$scrollbarThumbColorOverlayHov: pullForward($scrollbarThumbColorOverlay, 2%);
$scrollbarThumbColorMenu: pullForward($colorMenuBg, 20%);
$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);
// Splitter
// All splitterD* values MUST both be either odd or even numbers
@@ -243,6 +241,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
// Palettes
$colorPaletteFg: pullForward($colorMenuBg, 30%);
$colorPaletteSelected: #fff;
$shdwPaletteFg: black 0 0 2px;
$shdwPaletteSelected: inset 0 0 0 1px #000;
// About Screen
$colorAboutLink: #84b3ff;

View File

@@ -201,15 +201,13 @@ $shdwItemTreeIcon: none;
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 7px;
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;
$scrollbarTrackColorBg: rgba(#000, 0.2);
$scrollbarThumbColor: darken($colorBodyBg, 50%);
$scrollbarThumbColorHov: $colorKey;
$scrollbarThumbColorOverlay: darken($colorOvrBg, 50%);
$scrollbarThumbColorOverlayHov: $scrollbarThumbColorHov;
$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);
$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);
// Splitter
// All splitterD* values MUST both be either odd or even numbers
@@ -243,6 +241,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
// Palettes
$colorPaletteFg: pullForward($colorMenuBg, 30%);
$colorPaletteSelected: #333;
$shdwPaletteFg: none;
$shdwPaletteSelected: inset 0 0 0 1px #fff;
// About Screen
$colorAboutLink: #84b3ff;

View File

@@ -94,6 +94,31 @@ define([
}
},
"extensions": {
"versions": [
{
"name": "Version",
"value": "@@version",
"priority": 999
},
{
"name": "Built",
"value": "@@timestamp",
"description": "The date on which this version of the client was built.",
"priority": 990
},
{
"name": "Revision",
"value": "@@revision",
"description": "A unique revision identifier for the client sources.",
"priority": 995
},
{
"name": "Branch",
"value": "@@branch",
"description": "The name of the branch that was used during the build.",
"priority": 994
}
],
"components": [
{
"provides": "objectService",

View File

@@ -1,7 +1,7 @@
$ueTimeConductorH: (25px, 16px, 20px); // Heights for Ticks, Data Visualization, Controls elements
$ueTimeConductorRtH: (25px, 3px, 20px); // Heights for elements in Real-time mode
$timeCondInputTimeSysDefW: 165px; // Default width for datetime value inputs
$timeCondInputDeltaDefW: 65px; // Default width for delta value inputs, typically 00:00:00
$timeCondInputDeltaDefW: 60px; // Default width for delta value inputs, typically 00:00:00
$timeCondTOIIconD: 12px; // height and width of icon used for TOI indicator
$timeCondTOIValOffset: 0px;
$ticksBlockerFadeW: 50px;

View File

@@ -162,6 +162,9 @@
.l-time-conductor-inputs {
pointer-events: auto;
}
input[type="text"] {
@include trans-prop-nice(padding, 250ms);
}
.time-range-input input[type="text"] {
width: $timeCondInputTimeSysDefW;
}
@@ -287,6 +290,18 @@
.l-time-conductor-inputs-holder {
.l-time-range-input-w {
input[type="text"]:not(.error) {
background: transparent;
box-shadow: none;
border-radius: 0;
padding-left: 0;
padding-right: 0;
&:hover,
&:focus {
@include nice-input();
padding: $inputTextP;
}
}
.icon-calendar {
display: none;
}
@@ -294,11 +309,8 @@
display: none;
}
&.end-date {
// Displays the current time
pointer-events: none;
input[type="text"] {
background: none;
box-shadow: none;
color: pullForward($colorTimeCondKeyBg, 5%);
margin-right: $interiorMargin;
tab-index: -1;

View File

@@ -19,8 +19,8 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="w-menu">
<div class="col menu-items">
<div class="contents">
<div class="pane left menu-items">
<ul>
<li ng-repeat="metadata in ngModel.options"
ng-click="ngModel.select(metadata)">
@@ -32,15 +32,13 @@
</li>
</ul>
</div>
<div class="col menu-item-description">
<div class="pane right menu-item-description">
<div class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
<div class="w-title-desc">
<div class="desc-area title">
{{ngModel.activeMetadata.name}}
</div>
<div class="desc-area description">
{{ngModel.activeMetadata.description}}
</div>
<div class="desc-area title">
{{ngModel.activeMetadata.name}}
</div>
<div class="desc-area description">
{{ngModel.activeMetadata.description}}
</div>
</div>
</div>

View File

@@ -24,7 +24,7 @@
ng-click="modeController.toggle()">
<span class="title-label">{{ngModel.selected.name}}</span>
</div>
<div class="menu super-menu mini l-mode-selector-menu"
<div class="menu super-menu mini mode-selector-menu"
ng-show="modeController.isActive()">
<mct-include key="'mode-menu'"
ng-model="ngModel">

View File

@@ -38,7 +38,7 @@
ng-model="boundsModel"
ng-blur="tcController.setOffsetsFromView(boundsModel)"
field="'startOffset'"
class="s-input-inline hrs-min-input">
class="hrs-min-input">
</mct-control>
</span>
</span>
@@ -71,7 +71,7 @@
ng-model="boundsModel"
ng-blur="tcController.setOffsetsFromView(boundsModel)"
field="'endOffset'"
class="s-input-inline hrs-min-input">
class="hrs-min-input">
</mct-control>
</span>
</span>

View File

@@ -201,7 +201,7 @@ define(
var options = [{
key: 'fixed',
name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.',
description: 'Query and explore data that falls between two fixed datetimes',
cssClass: 'icon-calendar'
}];
var clocks = {};

View File

@@ -19,11 +19,11 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="frame frame-template t-frame-inner abs t-object-type-{{ representation.selected.key }}">
<div class="frame frame-template t-frame-inner abs t-object-type-{{ domainObject.getModel().type }}">
<div class="abs object-browse-bar l-flex-row">
<div class="left flex-elem l-flex-row grows">
<mct-representation
key="'object-header-frame'"
key="'object-header'"
mct-object="domainObject"
class="l-flex-row flex-elem object-header grows">
</mct-representation>

View File

@@ -26,12 +26,8 @@
* @namespace platform/features/layout
*/
define(
[
'./LayoutDrag'
],
function (
LayoutDrag
) {
['./LayoutDrag'],
function (LayoutDrag) {
var DEFAULT_DIMENSIONS = [12, 8],
DEFAULT_GRID_SIZE = [32, 32],
@@ -128,10 +124,6 @@ define(
self.select(null, self.droppedIdToSelectAfterRefresh);
delete self.droppedIdToSelectAfterRefresh;
}
if (composition.indexOf(self.selectedId) === -1) {
self.clearSelection();
}
}
});
}

View File

@@ -424,17 +424,6 @@ define(
expect(selectedObj.showFrame).toEqual(jasmine.any(Function));
});
it("deselects the object that is no longer in the composition", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId());
var composition = ["b", "c"];
mockScope.$watchCollection.mostRecentCall.args[1](composition);
expect(controller.selected(childObj)).toBe(false);
});
});
}
);

View File

@@ -118,10 +118,7 @@ define([
"policies": [
{
"category": "view",
"implementation": PlotViewPolicy,
"depends": [
"openmct"
]
"implementation": PlotViewPolicy
}
],
"representations": [

View File

@@ -30,32 +30,30 @@ define(
* @constructor
* @memberof platform/features/plot
*/
function PlotViewPolicy(openmct) {
this.openmct = openmct;
function PlotViewPolicy() {
}
PlotViewPolicy.prototype.hasNumericTelemetry = function (domainObject) {
var adaptedObject = domainObject.useCapability('adapter');
function hasNumericTelemetry(domainObject) {
var telemetry = domainObject &&
domainObject.getCapability('telemetry'),
metadata = telemetry ? telemetry.getMetadata() : {},
ranges = metadata.ranges || [];
if (!adaptedObject.telemetry) {
return domainObject.hasCapability('delegation') &&
domainObject.getCapability('delegation')
.doesDelegateCapability('telemetry');
}
var metadata = this.openmct.telemetry.getMetadata(adaptedObject);
var rangeValues = metadata.valuesForHints(['range']);
if (rangeValues.length === 0) {
return false;
}
return !rangeValues.every(function (value) {
return value.format === 'string';
});
};
// Generally, we want to allow Plot for telemetry-providing
// objects (most telemetry is plottable.) We only want to
// suppress this for telemetry which only has explicitly
// non-numeric values.
return ranges.length === 0 || ranges.some(function (range) {
// Assume format is numeric if it is undefined
// (numeric telemetry is the common case)
return range.format === undefined ||
range.format === 'number';
});
}
PlotViewPolicy.prototype.allow = function (view, domainObject) {
if (view.key === 'plot') {
return this.hasNumericTelemetry(domainObject);
return hasNumericTelemetry(domainObject);
}
return true;

View File

@@ -27,97 +27,51 @@ define(
describe("Plot view policy", function () {
var testView,
mockDomainObject,
testAdaptedObject,
openmct,
telemetryMetadata,
mockTelemetry,
testMetadata,
policy;
beforeEach(function () {
testView = { key: "plot" };
testAdaptedObject = { telemetry: {} };
testMetadata = {};
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['useCapability', 'hasCapability', 'getCapability']
['getId', 'getModel', 'getCapability']
);
mockDomainObject.useCapability.andReturn(testAdaptedObject);
openmct = {
telemetry: jasmine.createSpyObj('telemetryAPI', [
'getMetadata'
])
};
telemetryMetadata = jasmine.createSpyObj('telemetryMetadata', [
'valuesForHints'
]);
telemetryMetadata.valuesForHints.andReturn([]);
openmct.telemetry.getMetadata.andReturn(telemetryMetadata);
policy = new PlotViewPolicy(openmct);
});
it('fetches metadata from telem api', function () {
policy.allow(testView, mockDomainObject);
expect(mockDomainObject.useCapability)
.toHaveBeenCalledWith('adapter');
expect(openmct.telemetry.getMetadata)
.toHaveBeenCalledWith(testAdaptedObject);
expect(telemetryMetadata.valuesForHints)
.toHaveBeenCalledWith(['range']);
});
it('returns false if no ranges exist', function () {
telemetryMetadata.valuesForHints.andReturn([]);
expect(policy.allow(testView, mockDomainObject)).toBe(false);
});
it('returns true if any ranges exist', function () {
telemetryMetadata.valuesForHints.andReturn([{}]);
expect(policy.allow(testView, mockDomainObject)).toBe(true);
});
it('returns false if all ranges are strings', function () {
telemetryMetadata.valuesForHints.andReturn([{
format: 'string'
}, {
format: 'string'
}]);
expect(policy.allow(testView, mockDomainObject)).toBe(false);
});
it('returns true if only some ranges are strings', function () {
telemetryMetadata.valuesForHints.andReturn([{
format: 'string'
}, {}]);
expect(policy.allow(testView, mockDomainObject)).toBe(true);
});
it('returns true for telemetry delegators', function () {
delete testAdaptedObject.telemetry;
mockDomainObject.hasCapability.andCallFake(function (c) {
return c === 'delegation';
mockTelemetry = jasmine.createSpyObj(
'telemetry',
['getMetadata']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return c === 'telemetry' ? mockTelemetry : undefined;
});
mockDomainObject.getCapability.andReturn(
jasmine.createSpyObj('delegation', [
'doesDelegateCapability'
])
);
mockDomainObject.getCapability('delegation')
.doesDelegateCapability.andCallFake(function (c) {
return c === 'telemetry';
});
expect(policy.allow(testView, mockDomainObject)).toBe(true);
expect(openmct.telemetry.getMetadata).not.toHaveBeenCalled();
mockTelemetry.getMetadata.andReturn(testMetadata);
policy = new PlotViewPolicy();
});
it('returns true for non-telemetry non-delegators', function () {
delete testAdaptedObject.telemetry;
mockDomainObject.hasCapability.andReturn(false);
expect(policy.allow(testView, mockDomainObject)).toBe(false);
it("allows the imagery view for domain objects with numeric telemetry", function () {
testMetadata.ranges = [{ key: "foo", format: "number" }];
expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
});
it("allows the imagery view for domain objects with unspecified telemetry", function () {
testMetadata.ranges = [{ key: "foo" }];
expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
});
it("disallows the imagery view for domain objects without image telemetry", function () {
testMetadata.ranges = [{ key: "foo", format: "somethingElse" }];
expect(policy.allow(testView, mockDomainObject)).toBeFalsy();
});
it("allows other views", function () {
testView.key = "somethingElse";
expect(policy.allow(testView, mockDomainObject)).toBe(true);
testMetadata.ranges = [{ key: "foo", format: "somethingElse" }];
expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
});
});
}
);

View File

@@ -24,21 +24,22 @@
<span class="l-click-area" ng-click="toggle.toggle()"></span>
<span class="color-swatch"
ng-class="{'no-selection':ngModel[field] === 'transparent'}"
ng-style="{
background: ngModel[field]
'background-color': ngModel[field]
}">
</span>
<span class="title-label" ng-if="structure.text">
{{structure.text}}
</span>
<div class="menu l-color-palette"
<div class="menu l-palette l-color-palette"
ng-controller="ColorController as colors"
ng-show="toggle.isActive()">
<div
class="l-palette-row l-option-row"
ng-if="!structure.mandatory">
<div class="l-palette-item s-palette-item {{ngModel[field] === 'transparent' ? 'icon-check' : '' }}"
<div class="l-palette-item s-palette-item no-selection {{ngModel[field] === 'transparent' ? 'selected' : '' }}"
ng-click="ngModel[field] = 'transparent'">
</div>
<span class="l-palette-item-label">None</span>
@@ -46,7 +47,7 @@
<div
class="l-palette-row"
ng-repeat="group in colors.groups()">
<div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'icon-check' : '' }}"
<div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'selected' : '' }}"
ng-repeat="color in group"
ng-style="{ background: color }"
ng-click="ngModel[field] = color">

View File

@@ -47,7 +47,7 @@ define([
"name": "Export as JSON",
"implementation": ExportAsJSONAction,
"category": "contextual",
"cssClass": "icon-export",
"cssClass": "icon-save",
"depends": [
"exportService",
"policyService",
@@ -59,7 +59,7 @@ define([
"name": "Import from JSON",
"implementation": ImportAsJSONAction,
"category": "contextual",
"cssClass": "icon-import",
"cssClass": "icon-download",
"depends": [
"exportService",
"identifierService",

View File

@@ -25,12 +25,10 @@
*/
define(
[
'../../../src/api/objects/object-utils',
'lodash'
'../../../src/api/objects/object-utils'
],
function (
objectUtils,
_
objectUtils
) {
var ZERO = function () {
@@ -191,17 +189,14 @@ define(
return fullRequest;
};
function asSeries(telemetry, defaultDomain, defaultRange, sourceMap) {
function getValue(index, key) {
return telemetry[index][sourceMap[key].source];
}
function asSeries(telemetry, defaultDomain, defaultRange) {
return {
getRangeValue: function (index, range) {
return getValue(index, range || defaultRange);
return telemetry[index][range || defaultRange];
},
getDomainValue: function (index, domain) {
return getValue(index, domain || defaultDomain);
return telemetry[index][domain || defaultDomain];
},
getPointCount: function () {
return telemetry.length;
@@ -228,11 +223,9 @@ define(
var telemetryAPI = this.openmct.telemetry;
var metadata = telemetryAPI.getMetadata(domainObject);
var defaultDomain = metadata.valuesForHints(['domain'])[0].key;
var defaultDomain = metadata.valuesForHints(['domain'])[0].source;
var defaultRange = metadata.valuesForHints(['range'])[0];
defaultRange = defaultRange ? defaultRange.key : undefined;
var sourceMap = _.indexBy(metadata.values(), 'key');
defaultRange = defaultRange ? defaultRange.source : undefined;
var isLegacyProvider = telemetryAPI.findRequestProvider(domainObject) ===
telemetryAPI.legacyProvider;
@@ -257,7 +250,7 @@ define(
requestTelemetryFromService().then(getRelevantResponse);
} else {
return telemetryAPI.request(domainObject, fullRequest).then(function (telemetry) {
return asSeries(telemetry, defaultDomain, defaultRange, sourceMap);
return asSeries(telemetry, defaultDomain, defaultRange);
});
}
};
@@ -293,17 +286,15 @@ define(
var telemetryAPI = this.openmct.telemetry;
var metadata = telemetryAPI.getMetadata(domainObject);
var defaultDomain = metadata.valuesForHints(['domain'])[0].key;
var defaultDomain = metadata.valuesForHints(['domain'])[0].source;
var defaultRange = metadata.valuesForHints(['range'])[0];
defaultRange = defaultRange ? defaultRange.key : undefined;
var sourceMap = _.indexBy(metadata.values(), 'key');
defaultRange = defaultRange ? defaultRange.source : undefined;
var isLegacyProvider = telemetryAPI.findSubscriptionProvider(domainObject) ===
telemetryAPI.legacyProvider;
function update(telemetry) {
callback(asSeries([telemetry], defaultDomain, defaultRange, sourceMap));
callback(asSeries([telemetry], defaultDomain, defaultRange));
}
// Unpack the relevant telemetry series

View File

@@ -34,7 +34,6 @@ define(
mockUnsubscribe,
telemetry,
mockTelemetryAPI,
mockMetadata,
mockAPI;
function mockPromise(value) {
@@ -91,23 +90,14 @@ define(
"findRequestProvider",
"findSubscriptionProvider"
]);
mockMetadata = jasmine.createSpyObj('telemetryMetadata', [
'valuesForHints',
'values'
]);
mockMetadata.valuesForHints.andCallFake(function (hints) {
var hint = hints[0];
var metadatum = {
key: 'default' + hint
};
metadatum[hint] = "foo";
return [metadatum];
mockTelemetryAPI.getMetadata.andReturn({
valuesForHints: function (hint) {
var metadatum = {};
metadatum[hint] = "foo";
return [metadatum];
}
});
mockTelemetryAPI.getMetadata.andReturn(mockMetadata);
mockAPI = {
telemetry: mockTelemetryAPI,
time: {
@@ -160,8 +150,8 @@ define(
key: "testKey", // from model
start: 42, // from argument
domain: 'mockTimeSystem',
domains: [{ domain: "foo", key: 'defaultdomain' }],
ranges: [{ range: "foo", key: 'defaultrange' }]
domains: [{ domain: "foo" }],
ranges: [{ range: "foo" }]
}]);
});
@@ -182,8 +172,8 @@ define(
start: 0,
end: 1,
domain: 'mockTimeSystem',
domains: [{ domain: "foo", key: 'defaultdomain' }],
ranges: [{ range: "foo", key: 'defaultrange' }]
domains: [{ domain: "foo" }],
ranges: [{ range: "foo" }]
});
});
@@ -201,8 +191,8 @@ define(
start: 0,
end: 1,
domain: 'mockTimeSystem',
domains: [{ domain: "foo", key: 'defaultdomain' }],
ranges: [{ range: "foo", key: 'defaultrange' }]
domains: [{ domain: "foo" }],
ranges: [{ range: "foo" }]
});
});
@@ -250,21 +240,6 @@ define(
var mockProvider = {};
var dunzo = false;
mockMetadata.values.andReturn([
{
key: 'defaultrange',
source: 'prop1'
},
{
key: 'defaultdomain',
source: 'prop2'
},
{
key: 'prop3',
source: 'prop3'
}
]);
mockTelemetryAPI.findRequestProvider.andReturn(mockProvider);
mockTelemetryAPI.request.andReturn(Promise.resolve(mockTelemetry));
@@ -282,18 +257,6 @@ define(
expect(returnedTelemetry.getDomainValue).toBeDefined();
expect(returnedTelemetry.getRangeValue).toBeDefined();
expect(returnedTelemetry.getPointCount()).toBe(2);
// Default domain + remap should work.
expect(returnedTelemetry.getDomainValue(0)).toBe('val2');
expect(returnedTelemetry.getDomainValue(1)).toBe('val5');
// explicit domain should work
expect(returnedTelemetry.getDomainValue(0, 'prop3')).toBe('val3');
expect(returnedTelemetry.getDomainValue(1, 'prop3')).toBe('val6');
// default range + remap should work
expect(returnedTelemetry.getRangeValue(0)).toBe('val1');
expect(returnedTelemetry.getRangeValue(1)).toBe('val4');
// explicit range should work
expect(returnedTelemetry.getRangeValue(0, 'prop3')).toBe('val3');
expect(returnedTelemetry.getRangeValue(1, 'prop3')).toBe('val6');
});
});
@@ -312,8 +275,8 @@ define(
start: 0,
end: 1,
domain: 'mockTimeSystem',
domains: [{ domain: "foo", key: "defaultdomain" }],
ranges: [{ range: "foo", key: "defaultrange" }]
domains: [{ domain: "foo" }],
ranges: [{ range: "foo" }]
}]
);

View File

@@ -106,9 +106,9 @@ define([
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name mainViews
* @name objectViews
*/
this.mainViews = new ViewRegistry();
this.objectViews = new ViewRegistry();
/**
* Registry for views which should appear in the Inspector area.
@@ -255,6 +255,19 @@ define([
this.legacyExtension('types', legacyDefinition);
}.bind(this));
this.objectViews.providers.forEach(function (p) {
this.legacyExtension('views', {
key: 'vpid' + p.vpid,
vpid: p.vpid,
provider: p,
name: p.name,
cssClass: p.cssClass,
description: p.description,
editable: p.editable,
template: '<mct-view mct-vpid="' + p.vpid + '"/>'
});
}, this);
legacyRegistry.register('adapter', this.legacyBundle);
legacyRegistry.enable('adapter');
/**

7
src/about.frag Normal file
View File

@@ -0,0 +1,7 @@
/**
* Open MCT https://nasa.github.io/openmct/
* Version: ${version}
* Built: ${timestamp}
* Revision: ${revision}
* Branch: ${branch}
*/

View File

@@ -24,7 +24,6 @@ define([
'legacyRegistry',
'./actions/ActionDialogDecorator',
'./capabilities/AdapterCapability',
'./controllers/AdaptedViewController',
'./directives/MCTView',
'./services/Instantiate',
'./services/MissingModelCompatibilityDecorator',
@@ -32,13 +31,11 @@ define([
'./policies/AdapterCompositionPolicy',
'./policies/AdaptedViewPolicy',
'./runs/AlternateCompositionInitializer',
'./runs/TimeSettingsURLHandler',
'text!./templates/adapted-view-template.html'
'./runs/TimeSettingsURLHandler'
], function (
legacyRegistry,
ActionDialogDecorator,
AdapterCapability,
AdaptedViewController,
MCTView,
Instantiate,
MissingModelCompatibilityDecorator,
@@ -46,15 +43,15 @@ define([
AdapterCompositionPolicy,
AdaptedViewPolicy,
AlternateCompositionInitializer,
TimeSettingsURLHandler,
adaptedViewTemplate
TimeSettingsURLHandler
) {
legacyRegistry.register('src/adapter', {
"extensions": {
"directives": [
{
key: "mctView",
implementation: MCTView
implementation: MCTView,
depends: ["openmct"]
}
],
capabilities: [
@@ -63,16 +60,6 @@ define([
implementation: AdapterCapability
}
],
controllers: [
{
key: "AdaptedViewController",
implementation: AdaptedViewController,
depends: [
'$scope',
'openmct'
]
}
],
services: [
{
key: "instantiate",
@@ -135,12 +122,6 @@ define([
depends: ["openmct", "$location", "$rootScope"]
}
],
views: [
{
key: "adapted-view",
template: adaptedViewTemplate
}
],
licenses: [
{
"name": "almond",

View File

@@ -22,10 +22,12 @@
define([
'./synchronizeMutationCapability',
'./AlternateCompositionCapability'
'./AlternateCompositionCapability',
'./patchViewCapability'
], function (
synchronizeMutationCapability,
AlternateCompositionCapability
AlternateCompositionCapability,
patchViewCapability
) {
/**
@@ -46,6 +48,9 @@ define([
capabilities.mutation =
synchronizeMutationCapability(capabilities.mutation);
}
if (capabilities.view) {
capabilities.view = patchViewCapability(capabilities.view);
}
if (AlternateCompositionCapability.appliesTo(model, id)) {
capabilities.composition = function (domainObject) {
return new AlternateCompositionCapability(this.$injector, domainObject);

View File

@@ -21,30 +21,41 @@
*****************************************************************************/
define([
'./plugin'
], function (plugin) {
describe("The buildInfo plugin", function () {
var mockmct;
var testInfo;
'lodash'
], function (
_
) {
beforeEach(function () {
mockmct = jasmine.createSpyObj('openmct', ['legacyExtension']);
testInfo = { foo: 123, bar: "baz" };
plugin(testInfo)(mockmct);
});
function patchViewCapability(viewConstructor) {
return function makeCapability(domainObject) {
var capability = viewConstructor(domainObject);
var oldInvoke = capability.invoke.bind(capability);
it("registers versions extensions", function () {
Object.keys(testInfo).forEach(function (key) {
expect(mockmct.legacyExtension).toHaveBeenCalledWith(
"versions",
{
key: key,
name: jasmine.any(String),
value: testInfo[key],
description: undefined
capability.invoke = function () {
var availableViews = oldInvoke();
var newDomainObject = capability
.domainObject
.useCapability('adapter');
return _(availableViews).map(function (v, i) {
var vd = {
view: v,
priority: i + 100 // arbitrary to allow new views to
// be defaults by returning priority less than 100.
};
if (v.provider) {
vd.priority = v.provider.canView(newDomainObject);
}
);
});
});
});
return vd;
})
.sortBy('priority')
.map('view')
.value();
};
return capability;
};
}
return patchViewCapability;
});

View File

@@ -21,19 +21,20 @@
*****************************************************************************/
define([
'angular',
'./Region'
], function (
angular,
Region
) {
function MCTView() {
function MCTView(openmct) {
return {
restrict: 'A',
restrict: 'E',
link: function (scope, element, attrs) {
var region = new Region(element[0]);
scope.$watch(attrs.mctView, region.show.bind(region));
scope.$on("$destroy", region.clear.bind(region));
var provider = openmct.objectViews.getByVPID(Number(attrs.mctVpid));
var view = new provider.view(scope.domainObject.useCapability('adapter'));
view.show(element[0]);
if (view.destroy) {
scope.$on('$destroy', function () {
view.destroy(element[0]);
});
}
}
};
}

View File

@@ -1,45 +0,0 @@
/*****************************************************************************
* 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 Region(element) {
this.activeView = undefined;
this.element = element;
}
Region.prototype.clear = function () {
if (this.activeView) {
this.activeView.destroy();
this.activeView = undefined;
}
};
Region.prototype.show = function (view) {
this.clear();
this.activeView = view;
if (this.activeView) {
this.activeView.show(this.element);
}
};
return Region;
});

View File

@@ -29,9 +29,9 @@ define([], function () {
view,
legacyObject
) {
if (view.key === 'adapted-view') {
if (view.hasOwnProperty('vpid')) {
var domainObject = legacyObject.useCapability('adapter');
return this.openmct.mainViews.get(domainObject).length > 0;
return view.provider.canView(domainObject);
}
return true;
};

View File

@@ -4,7 +4,7 @@
<a class="close icon-x-in-circle"></a>
<div class="abs inner-holder contents">
<div class="abs top-bar">
<div class="title"></div>
<div class="dialog-title"></div>
<div class="hint"></div>
</div>
<div class='abs editor'>

View File

@@ -1,45 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 (
) {
return function (buildInfo) {
return function (openmct) {
var aliases = { timestamp: "Built" };
var descriptions = {
timestamp: "The date on which this version of Open MCT was built.",
revision: "A unique revision identifier for the client sources.",
branch: "The name of the branch that was used during the build."
};
Object.keys(buildInfo).forEach(function (key) {
openmct.legacyExtension("versions", {
key: key,
name: aliases[key] || (key.charAt(0).toUpperCase() + key.substring(1)),
value: buildInfo[key],
description: descriptions[key]
});
});
};
};
});

View File

@@ -27,7 +27,8 @@ define([
'../../platform/features/autoflow/plugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
'../../platform/import-export/bundle'
'../../platform/import-export/bundle',
'./summaryWidget/plugin'
], function (
_,
UTCTimeSystem,
@@ -35,6 +36,7 @@ define([
AutoflowPlugin,
TimeConductorPlugin,
ExampleImagery,
SummaryWidget,
ImportExport
) {
var bundleMap = {
@@ -121,5 +123,7 @@ define([
plugins.ExampleImagery = ExampleImagery;
plugins.SummaryWidget = SummaryWidget;
return plugins;
});

View File

@@ -20,21 +20,33 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
function AdaptedViewController($scope, openmct) {
function refresh(legacyObject) {
if (!legacyObject) {
$scope.view = undefined;
return;
}
define(
[],
function () {
var domainObject = legacyObject.useCapability('adapter');
var providers = openmct.mainViews.get(domainObject);
$scope.view = providers[0] && providers[0].view(domainObject);
/**
* Defines composition policy for Display Layout objects.
* They cannot contain folders.
* @constructor
* @memberof platform/features/layout
* @implements {Policy.<View, DomainObject>}
*/
function SummaryWidgetsCompositionPolicy(openmct) {
this.openmct = openmct;
}
$scope.$watch('domainObject', refresh);
}
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {
return AdaptedViewController;
});
var parentType = parent.getCapability('type');
var newStyleChild = child.useCapability('adapter');
if (parentType.instanceOf('summary-widget') && !this.openmct.telemetry.canProvideTelemetry(newStyleChild)) {
return false;
}
return true;
};
return SummaryWidgetsCompositionPolicy;
}
);

View File

@@ -0,0 +1,42 @@
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 = {};
}
};
function initViewProvider(openmct) {
return {
name: 'Widget View',
view: function (domainObject) {
var summaryWidget = new SummaryWidget(domainObject, openmct);
return {
show: summaryWidget.show,
destroy: summaryWidget.destroy
};
},
canView: function (domainObject) {
return (domainObject.type === 'summary-widget');
},
editable: true
};
}
return function install(openmct) {
openmct.types.addType('summary-widget', widgetType);
openmct.objectViews.addProvider(initViewProvider(openmct));
openmct.legacyExtension('policies', {category: 'composition',
implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct']});
};
}
return plugin;
});

View File

@@ -0,0 +1,11 @@
<li class="t-condition">
<label>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"></a>
<a class="s-icon-button icon-trash t-delete"></a>
</span>
</li>

View 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>

View File

@@ -0,0 +1,4 @@
<div class="e-control select">
<select>
</select>
</div>

View 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>

View File

@@ -0,0 +1,73 @@
<div>
<div class="l-widget-rule s-widget-rule l-compact-form">
<div class="widget-rule-header">
<span class="flex-elem l-widget-thumb-wrapper">
<span class="grippy-holder">
<span class="t-grippy grippy"></span>
</span>
<span class="view-control expanded"></span>
<span class="t-widget-thumb widget-thumb">
<span class="widget-label">DEF</span>
</span>
</span>
<span class="flex-elem rule-title">Default Title</span>
<span class="flex-elem rule-description grows">Rule description goes here</span>
<span class="flex-elem l-rule-action-buttons-wrapper">
<a class="s-icon-button icon-duplicate t-duplicate"></a>
<a class="s-icon-button icon-trash t-delete"></a>
</span>
</div>
<div class="widget-rule-content expanded">
<ul>
<li>
<label>Rule Name:</label>
<span class="controls">
<input class="t-rule-name-input" type="text" />
</span>
</li>
<li class="connects-to-previous">
<label>Label:</label>
<span class="controls t-label-input">
<input class="e-control t-rule-label-input" type="text" />
</span>
</li>
<li class="connects-to-previous">
<label>Message:</label>
<span class="controls">
<input type="text" class="lg s t-rule-message-input"
placeholder="Will appear as tooltip when hovering on the widget"/>
</span>
</li>
<li class="connects-to-previous">
<label>Style:</label>
<span class="controls t-style-input">
</span>
</li>
</ul>
<ul class="t-widget-rule-config">
<li>
<label>Trigger when</label>
<span class="controls">
<div class="e-control select">
<select class="t-trigger">
<option value="any">any condition is met</option>
<option value="all">all conditions are met</option>
<option value="js">the following JavaScript evaluates to true</option>
</select>
</div>
</span>
</li>
<li class="t-rule-js-condition-input-holder">
<textarea placeholder="" class="med t-rule-js-condition-input"></textarea>
</li>
<li>
<label></label>
<span class="controls">
<a class="e-control s-button labeled add-condition icon-plus">Add Condition</a>
</span>
</li>
</ul>
</div>
</div>
<div class="t-drag-indicator l-widget-rule s-widget-rule" style="opacity:0;" hidden></div>
</div>

View 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"></a>
<a class="s-icon-button icon-trash t-delete"></a>
</span>
</li>
</ul>
</div>

View File

@@ -0,0 +1,16 @@
<div>
<div class="section-header"><span class="view-control expanded"></span>Test Data Values</div>
<div class="t-widget-test-data-content w-widget-test-data-content expanded">
<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>

View File

@@ -0,0 +1,22 @@
<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 flex-elem grows widget-edit-holder">
<div class="widget-test-data"></div>
<div class="section-header">Rules</div>
<div class="holder widget-rules-wrapper flex-elem grows">
<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>

View File

@@ -0,0 +1,192 @@
define([
'text!../res/conditionTemplate.html',
'./input/ObjectSelect',
'./input/KeySelect',
'./input/OperationSelect',
'EventEmitter',
'zepto'
], function (
conditionTemplate,
ObjectSelect,
KeySelect,
OperationSelect,
EventEmitter,
$
) {
/**
* Represents an individual condition for a summary widget rule. Manages the
* associated inputs and view.
* @param {Object} conditionConfig The configurration for this condition, consisting
* of object, key, operation, and values fields
* @param {number} index the index of this Condition object in it's parent Rule's data model,
* to be injected into callbacks for removes
* @param {ConditionManager} conditionManager A ConditionManager instance for populating
* selects with configuration data
*/
function Condition(conditionConfig, index, conditionManager) {
this.config = conditionConfig;
this.index = index;
this.conditionManager = conditionManager;
this.domElement = $(conditionTemplate);
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
this.deleteButton = $('.t-delete', this.domElement);
this.duplicateButton = $('.t-duplicate', this.domElement);
this.selects = {};
this.valueInputs = [];
this.remove = this.remove.bind(this);
this.duplicate = this.duplicate.bind(this);
var self = this;
/**
* Event handler for a change in one of this conditions' custom selects
* @param {string} value The new value of this selects
* @param {string} property The property of this condition to modify
* @private
*/
function onSelectChange(value, property) {
if (property === 'operation') {
self.generateValueInputs(value);
}
self.eventEmitter.emit('change', {
value: value,
property: property,
index: self.index
});
}
/**
* Event handler for this conditions value inputs
* @param {Event} event The oninput event that triggered this callback
* @private
*/
function onValueInput(event) {
var elem = event.target,
value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber),
inputIndex = self.valueInputs.indexOf(elem);
self.eventEmitter.emit('change', {
value: value,
property: 'values[' + inputIndex + ']',
index: self.index
});
}
this.deleteButton.on('click', this.remove);
this.duplicateButton.on('click', this.duplicate);
this.selects.object = new ObjectSelect(this.config, this.conditionManager, [
['any', 'Any Telemetry'],
['all', 'All Telemetry']
]);
this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager);
this.selects.operation = new OperationSelect(
this.config,
this.selects.key,
this.conditionManager,
function (value) {
onSelectChange(value, 'operation');
});
this.selects.object.on('change', function (value) {
onSelectChange(value, 'object');
});
this.selects.key.on('change', function (value) {
onSelectChange(value, 'key');
});
Object.values(this.selects).forEach(function (select) {
$('.t-configuration', self.domElement).append(select.getDOM());
});
$(this.domElement).on('input', 'input', onValueInput);
}
/**
* Get the DOM element representing this condition in the view
* @return {Element}
*/
Condition.prototype.getDOM = function (container) {
return this.domElement;
};
/**
* Register a callback with this condition: supported callbacks are remove, change,
* duplicate
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will envoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
Condition.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
}
};
/**
* Hide the appropriate inputs when this is the only condition
*/
Condition.prototype.hideButtons = function () {
this.deleteButton.hide();
};
/**
* Remove this condition from the configuration. Invokes any registered
* remove callbacks
*/
Condition.prototype.remove = function () {
this.eventEmitter.emit('remove', this.index);
};
/**
* Make a deep clone of this condition's configuration and invoke any duplicate
* callbacks with the cloned configuration and this rule's index
*/
Condition.prototype.duplicate = function () {
var sourceCondition = JSON.parse(JSON.stringify(this.config));
this.eventEmitter.emit('duplicate', {
sourceCondition: sourceCondition,
index: this.index
});
};
/**
* When an operation is selected, create the appropriate value inputs
* and add them to the view
* @param {string} operation The key of currently selected operation
*/
Condition.prototype.generateValueInputs = function (operation) {
var evaluator = this.conditionManager.getEvaluator(),
inputArea = $('.t-value-inputs', this.domElement),
inputCount,
inputType,
newInput,
index = 0;
inputArea.html('');
this.valueInputs = [];
if (evaluator.getInputCount(operation)) {
inputCount = evaluator.getInputCount(operation);
inputType = evaluator.getInputType(operation);
while (index < inputCount) {
if (!this.config.values[index]) {
this.config.values[index] = (inputType === 'number' ? 0 : '');
}
newInput = $('<input class="sm" type = "' + inputType + '" value = "' + this.config.values[index] + '"> </input>');
this.valueInputs.push(newInput.get(0));
inputArea.append(newInput);
index += 1;
}
}
};
return Condition;
});

View File

@@ -0,0 +1,449 @@
define([], function () {
/**
* Responsible for maintaining the possible operations for conditions
* in this widget, and evaluating the boolean value of conditions passed as
* input.
* @constructor
* @param {Object} subscriptionCache A cache consisting of the latest available
* data for any telemetry sources in the widget's
* composition.
* @param {Object} compositionObjs The current set of composition objects to
* evaluate for 'any' and 'all' conditions
*/
function ConditionEvaluator(subscriptionCache, compositionObjs) {
this.subscriptionCache = subscriptionCache;
this.compositionObjs = compositionObjs;
this.testCache = {};
this.useTestCache = false;
/**
* Maps value types to HTML input field types. These
* type of inputs will be generated by conditions expecting this data type
*/
this.inputTypes = {
number: 'number',
string: 'text'
};
/**
* Functions to validate that the input to an operation is of the type
* that it expects, in order to prevent unexpected behavior. Will be
* invoked before the corresponding operation is executed
*/
this.inputValidators = {
number: this.validateNumberInput,
string: this.validateStringInput
};
/**
* A library of operations supported by this rule evaluator. Each operation
* consists of the following fields:
* operation: a function with boolean return type to be invoked when this
* operation is used. Will be called with an array of inputs
* where input [0] is the telemetry value and input [1..n] are
* any comparison values
* text: a human-readable description of this operation to populate selects
* appliesTo: an array of identifiers for types that operation may be used on
* inputCount: the number of inputs required to get any necessary comparison
* values for the operation
* getDescription: A function returning a human-readable shorthand description of
* this operation to populate the 'description' field in the rule header.
* Will be invoked with an array of a condition's comparison values.
*/
this.operations = {
equalTo: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is Equal To',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
},
notEqualTo: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is Not Equal To',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
},
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is Greater Than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is Less Than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values[0];
}
},
greaterThanOrEq: {
operation: function (input) {
return input[0] >= input[1];
},
text: 'is Greater Than or Equal To',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values[0];
}
},
lessThanOrEq: {
operation: function (input) {
return input[0] <= input[1];
},
text: 'is Less Than or Equal To',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values[0];
}
},
between: {
operation: function (input) {
return input[0] > input[1] && input[0] < input[2];
},
text: 'is Between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' between ' + values[0] + ' and ' + values[1];
}
},
notBetween: {
operation: function (input) {
return input[0] < input[1] || input[0] > input[2];
},
text: 'is Not Between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' not between ' + values[0] + ' and ' + values[1];
}
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'Text Contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values[0];
}
},
textDoesNotContain: {
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'Text Does Not Contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values[0];
}
},
textStartsWith: {
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'Text Starts With',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values[0];
}
},
textEndsWith: {
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'Text Ends With',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values[0];
}
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'Text is Exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values[0];
}
},
isUndefined: {
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is Undefined',
appliesTo: ['string', 'number'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
}
};
}
/**
* Evaluate the conditions passed in as an argument, and return the boolean
* value of these conditions. Available evaluation modes are 'any', which will
* return true if any of the conditions evaluates to true (i.e. logical OR); 'all',
* which returns true only if all conditions evalute to true (i.e. logical AND);
* or 'js', which returns the boolean value of a custom JavaScript conditional.
* @param {} conditions Either an array of objects with object, key, operation,
* and value fields, or a string representing a JavaScript
* condition.
* @param {string} mode The key of the mode to use when evaluating the conditions.
* @return {boolean} The boolean value of the conditions
*/
ConditionEvaluator.prototype.execute = function (conditions, mode) {
var active = false,
conditionValue,
conditionDefined = false,
self = this,
firstRuleEvaluated = false,
compositionObjs = this.compositionObjs;
if (mode === 'js') {
active = this.executeJavaScriptCondition(conditions);
} else {
(conditions || []).forEach(function (condition) {
conditionDefined = false;
if (condition.object === 'any') {
conditionValue = false;
Object.keys(compositionObjs).forEach(function (objId) {
try {
conditionValue = conditionValue ||
self.executeCondition(objId, condition.key,
condition.operation, condition.values);
conditionDefined = true;
} catch (e) {
//ignore a malformed condition
}
});
} else if (condition.object === 'all') {
conditionValue = true;
Object.keys(compositionObjs).forEach(function (objId) {
try {
conditionValue = conditionValue &&
self.executeCondition(objId, condition.key,
condition.operation, condition.values);
conditionDefined = true;
} catch (e) {
//ignore a malformed condition
}
});
} else {
try {
conditionValue = self.executeCondition(condition.object, condition.key,
condition.operation, condition.values);
conditionDefined = true;
} catch (e) {
//ignore malformed condition
}
}
if (conditionDefined) {
active = (mode === 'all' && !firstRuleEvaluated ? true : active);
firstRuleEvaluated = true;
if (mode === 'any') {
active = active || conditionValue;
} else if (mode === 'all') {
active = active && conditionValue;
}
}
});
}
return active;
};
/**
* Execute a condition defined as an object.
* @param {string} object The identifier of the telemetry object to retrieve data from
* @param {string} key The property of the telemetry object
* @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition
* @param {string} values An array of comparison values to invoke the operation with
* @return {boolean} The value of this condition
*/
ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) {
var cache = (this.useTestCache ? this.testCache : this.subscriptionCache),
telemetryValue,
op,
input,
validator;
if (cache[object] && typeof cache[object][key] !== 'undefined') {
telemetryValue = [cache[object][key]];
}
op = this.operations[operation] && this.operations[operation].operation;
input = telemetryValue && telemetryValue.concat(values);
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
if (op && input && validator) {
return validator(input) && op(input);
} else {
throw new Error('Malformed condition');
}
};
/**
* Interpret a string as a JavaScript conditional, and return its boolean value
* @param {string} condition The string to interpreted as JavaScript
* @return {boolean} The value of the conditions
*/
ConditionEvaluator.prototype.executeJavaScriptCondition = function (condition) {
var conditionValue = false;
//TODO: implement JavaScript execution
return conditionValue;
};
/**
* A function that returns true only if each value in its input argument is
* of a numerical type
* @param {[]} input An array of values
* @returns {boolean}
*/
ConditionEvaluator.prototype.validateNumberInput = function (input) {
var valid = true;
input.forEach(function (value) {
valid = valid && (typeof value === 'number');
});
return valid;
};
/**
* A function that returns true only if each value in its input argument is
* a string
* @param {[]} input An array of values
* @returns {boolean}
*/
ConditionEvaluator.prototype.validateStringInput = function (input) {
var valid = true;
input.forEach(function (value) {
valid = valid && (typeof value === 'string');
});
return valid;
};
/**
* Get the keys of operations supported by this evaluator
* @return {string[]} An array of the keys of supported operations
*/
ConditionEvaluator.prototype.getOperationKeys = function () {
return Object.keys(this.operations);
};
/**
* Get the human-readable text corresponding to a given operation
* @param {string} key The key of the operation
* @return {string} The text description of the operation
*/
ConditionEvaluator.prototype.getOperationText = function (key) {
return this.operations[key].text;
};
/**
* Returns true only of the given operation applies to a given type
* @param {string} key The key of the operation
* @param {string} type The value type to query
* @returns {boolean} True if the condition applies, false otherwise
*/
ConditionEvaluator.prototype.operationAppliesTo = function (key, type) {
return (this.operations[key].appliesTo.includes(type));
};
/**
* Return the number of value inputs required by an operation
* @param {string} key The key of the operation to query
* @return {number}
*/
ConditionEvaluator.prototype.getInputCount = function (key) {
if (this.operations[key]) {
return this.operations[key].inputCount;
}
};
/**
* Return the human-readable shorthand description of the operation for a rule header
* @param {string} key The key of the operation to query
* @param {} values An array of values with which to invoke the getDescription function
* of the operation
* @return {string} A text description of this operation
*/
ConditionEvaluator.prototype.getOperationDescription = function (key, values) {
if (this.operations[key]) {
return this.operations[key].getDescription(values);
}
};
/**
* Return the HTML input type associated with a given operation
* @param {string} key The key of the operation to query
* @return {string} The key for an HTML5 input type
*/
ConditionEvaluator.prototype.getInputType = function (key) {
var type;
if (this.operations[key]) {
type = this.operations[key].appliesTo[0];
}
if (this.inputTypes[type]) {
return this.inputTypes[type];
}
};
/**
* Returns the HTML input type associated with a value type
* @param {string} dataType The JavaScript value type
* @return {string} The key for an HTML5 input type
*/
ConditionEvaluator.prototype.getInputTypeById = function (dataType) {
return this.inputTypes[dataType];
};
/**
* Set the test data cache used by this rule evaluator
* @param {object} testCache A mock cache following the format of the real
* subscription cache
*/
ConditionEvaluator.prototype.setTestDataCache = function (testCache) {
this.testCache = testCache;
};
/**
* Have this RuleEvaluator pull data values from the provided test cache
* instead of its actual subscription cache when evaluating. If invoked with true,
* will use the test cache; otherwise, will use the subscription cache
* @param {boolean} useTestData Boolean flag
*/
ConditionEvaluator.prototype.useTestData = function (useTestCache) {
this.useTestCache = useTestCache;
};
return ConditionEvaluator;
});

View File

@@ -0,0 +1,372 @@
define ([
'./ConditionEvaluator',
'EventEmitter',
'zepto',
'lodash'
], function (
ConditionEvaluator,
EventEmitter,
$,
_
) {
/**
* Provides a centralized content manager for conditions in the summary widget.
* Loads and caches composition and telemetry subscriptions, and maintains a
* {ConditionEvaluator} instance to handle evaluation
* @constructor
* @param {Object} domainObject the Summary Widget domain object
* @param {MCT} openmct an MCT instance
*/
function ConditionManager(domainObject, openmct) {
this.domainObject = domainObject;
this.openmct = openmct;
this.composition = this.openmct.composition.get(this.domainObject);
this.compositionObjs = {};
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry'];
this.keywordLabels = {
any: 'Any Telemetry',
all: 'All Telemetry'
};
this.telemetryMetadataById = {
any: {},
all: {}
};
this.telemetryTypesById = {
any: {},
all: {}
};
this.subscriptions = {};
this.subscriptionCache = {};
this.loadComplete = false;
this.metadataLoadComplete = false;
this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs);
this.composition.on('add', this.onCompositionAdd, this);
this.composition.on('remove', this.onCompositionRemove, this);
this.composition.on('load', this.onCompositionLoad, this);
this.composition.load();
}
/**
* Register a callback with this ConditionManager: supported callbacks are add
* remove, load, metadata, and receiveTelemetry
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will envoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
ConditionManager.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
}
};
/**
* Given a set of rules, execute the conditions associated with each rule
* and return the id of the last rule whose conditions evaluate to true
* @param {string[]} ruleOrder An array of rule IDs indicating what order They
* should be evaluated in
* @param {Object} rules An object mapping rule IDs to rule configurations
* @return {string} The ID of the rule to display on the widget
*/
ConditionManager.prototype.executeRules = function (ruleOrder, rules) {
var self = this,
activeId = ruleOrder[0],
rule,
conditions;
ruleOrder.forEach(function (ruleId) {
rule = rules[ruleId];
conditions = rule.getProperty('trigger') === 'js' ?
rule.getProperty('jsCondition') : rule.getProperty('conditions');
if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) {
activeId = ruleId;
}
});
return activeId;
};
/**
* Adds a field to the list of all available metadata fields in the widget
* @param {Object} metadatum An object representing a set of telemetry metadata
*/
ConditionManager.prototype.addGlobalMetadata = function (metadatum) {
this.telemetryMetadataById.any[metadatum.key] = metadatum;
this.telemetryMetadataById.all[metadatum.key] = metadatum;
};
/**
* Adds a field to the list of properties for globally available metadata
* @param {string} key The key for the property this type applies to
* @param {string} type The type that should be associated with this property
*/
ConditionManager.prototype.addGlobalPropertyType = function (key, type) {
this.telemetryTypesById.any[key] = type;
this.telemetryTypesById.all[key] = type;
};
/**
* Given a telemetry-producing domain object, associate each of it's telemetry
* fields with a type, parsing from historical data.
* @param {Object} object a domain object that can produce telemetry
* @return {Promise} A promise that resolves when a telemetry request
* has completed and types have been parsed
*/
ConditionManager.prototype.parsePropertyTypes = function (object) {
var telemetryAPI = this.openmct.telemetry,
key,
type,
self = this;
self.telemetryTypesById[object.identifier.key] = {};
return telemetryAPI.request(object, {}).then(function (telemetry) {
Object.entries(telemetry[telemetry.length - 1]).forEach(function (telem) {
key = telem[0];
type = typeof telem[1];
self.telemetryTypesById[object.identifier.key][key] = type;
self.subscriptionCache[object.identifier.key][key] = telem[1];
self.addGlobalPropertyType(key, type);
});
});
};
/**
* Parse types of telemetry fields from all composition objects; used internally
* to perform a block types load once initial composition load has completed
* @return {Promise} A promise that resolves when all metadata has been loaded
* and property types parsed
*/
ConditionManager.prototype.parseAllPropertyTypes = function () {
var self = this,
index = 0,
objs = Object.values(self.compositionObjs),
promise = new Promise(function (resolve, reject) {
if (objs.length === 0) {
resolve();
}
objs.forEach(function (obj) {
self.parsePropertyTypes(obj).then(function () {
if (index === objs.length - 1) {
resolve();
}
index += 1;
});
});
});
return promise;
};
/**
* Invoked when a telemtry subscription yields new data. Updates the LAD
* cache and invokes any registered receiveTelemetry callbacks
* @param {string} objId The key associated with the telemetry source
* @param {datum} datum The new data from the telemetry source
* @private
*/
ConditionManager.prototype.handleSubscriptionCallback = function (objId, datum) {
this.subscriptionCache[objId] = datum;
this.eventEmitter.emit('receiveTelemetry');
};
/**
* Event handler for an add event in this Summary Widget's composition.
* Sets up subscription handlers and parses its property types.
* @param {Object} obj The newly added domain object
* @private
*/
ConditionManager.prototype.onCompositionAdd = function (obj) {
var compositionKeys,
telemetryAPI = this.openmct.telemetry,
objId = obj.identifier.key,
telemetryMetadata,
self = this;
if (telemetryAPI.canProvideTelemetry(obj)) {
self.compositionObjs[objId] = obj;
self.telemetryMetadataById[objId] = {};
compositionKeys = self.domainObject.composition.map(function (object) {
return object.key;
});
if (!compositionKeys.includes(obj.identifier.key)) {
self.domainObject.composition.push(obj.identifier);
}
telemetryMetadata = telemetryAPI.getMetadata(obj).values();
telemetryMetadata.forEach(function (metaDatum) {
self.telemetryMetadataById[objId][metaDatum.key] = metaDatum;
self.addGlobalMetadata(metaDatum);
});
self.subscriptionCache[objId] = {};
self.subscriptions[objId] = telemetryAPI.subscribe(obj, function (datum) {
self.handleSubscriptionCallback(objId, datum);
}, {});
/**
* if this is the initial load, parsing property types will be postponed
* until all composition objects have been loaded
*/
if (self.loadComplete) {
self.parsePropertyTypes(obj);
}
self.eventEmitter.emit('add', obj);
$('.w-summary-widget').removeClass('s-status-no-data');
}
};
/**
* Invoked on a remove event in this Summary Widget's compostion. Removes
* the object from the local composition, and untracks it
* @param {object} identifier The identifier of the object to be removed
* @private
*/
ConditionManager.prototype.onCompositionRemove = function (identifier) {
_.remove(this.domainObject.composition, function (id) {
return id.key === identifier.key;
});
delete this.compositionObjs[identifier.key];
this.subscriptions[identifier.key](); //unsubscribe from telemetry source
this.eventEmitter.emit('remove', identifier);
if (_.isEmpty(this.compositionObjs)) {
$('.w-summary-widget').addClass('s-status-no-data');
}
};
/**
* Invoked when the Summary Widget's composition finishes its initial load.
* Invokes any registered load callbacks, does a block load of all metadata,
* and then invokes any registered metadata load callbacks.
* @private
*/
ConditionManager.prototype.onCompositionLoad = function () {
var self = this;
self.loadComplete = true;
self.eventEmitter.emit('load');
self.parseAllPropertyTypes().then(function () {
self.metadataLoadComplete = true;
self.eventEmitter.emit('metadata');
});
};
/**
* Returns the currently tracked telemetry sources
* @return {Object} An object mapping object keys to domain objects
*/
ConditionManager.prototype.getComposition = function () {
return this.compositionObjs;
};
/**
* Get the human-readable name of a domain object from its key
* @param {string} id The key of the domain object
* @return {string} The human-readable name of the domain object
*/
ConditionManager.prototype.getObjectName = function (id) {
var name;
if (this.keywordLabels[id]) {
name = this.keywordLabels[id];
} else if (this.compositionObjs[id]) {
name = this.compositionObjs[id].name;
}
return name;
};
/**
* Returns the property metadata associated with a given telemetry source
* @param {string} id The key associated with the domain object
* @return {Object} Returns an object with fields representing each telemetry field
*/
ConditionManager.prototype.getTelemetryMetadata = function (id) {
return this.telemetryMetadataById[id];
};
/**
* Returns the type associated with a telemtry data field of a particular domain
* object
* @param {string} id The key associated with the domain object
* @param {string} property The telemetry field key to retrieve the type of
* @return {string} The type name
*/
ConditionManager.prototype.getTelemetryPropertyType = function (id, property) {
if (this.telemetryTypesById[id]) {
return this.telemetryTypesById[id][property];
}
};
/**
* Returns the human-readable name of a telemtry data field of a particular domain
* object
* @param {string} id The key associated with the domain object
* @param {string} property The telemetry field key to retrieve the type of
* @return {string} The telemetry field name
*/
ConditionManager.prototype.getTelemetryPropertyName = function (id, property) {
if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) {
return this.telemetryMetadataById[id][property].name;
}
};
/**
* Returns the {ConditionEvaluator} instance associated with this condition
* manager
* @return {ConditionEvaluator}
*/
ConditionManager.prototype.getEvaluator = function () {
return this.evaluator;
};
/**
* Returns true if the initial compostion load has completed
* @return {boolean}
*/
ConditionManager.prototype.loadCompleted = function () {
return this.loadComplete;
};
/**
* Returns true if the initial block metadata load has completed
*/
ConditionManager.prototype.metadataLoadCompleted = function () {
return this.metadataLoadComplete;
};
/**
* Triggers the telemetryRecieve callbacks registered to this ConditionManager,
* used by the {TestDataManager} to force a rule evaluation when test data is
* enabled
*/
ConditionManager.prototype.triggerTelemetryCallback = function () {
this.eventEmitter.emit('receiveTelemetry');
};
/**
* Unsubscribe from all registered telemetry sources and unregister all event
* listeners registered with the Open MCT APIs
*/
ConditionManager.prototype.destroy = function () {
Object.values(this.subscriptions).forEach(function (unsubscribeFunction) {
unsubscribeFunction();
});
this.composition.off('add', this.onCompositionAdd, this);
this.composition.off('remove', this.onCompositionRemove, this);
this.composition.off('load', this.onCompositionLoad, this);
};
return ConditionManager;
});

View File

@@ -0,0 +1,458 @@
define([
'text!../res/ruleTemplate.html',
'./Condition',
'./input/ColorPalette',
'./input/IconPalette',
'EventEmitter',
'lodash',
'zepto'
], function (
ruleTemplate,
Condition,
ColorPalette,
IconPalette,
EventEmitter,
_,
$
) {
/**
* An object representing a summary widget rule. Maintains a set of text
* and css properties for output, and a set of conditions for configuring
* when the rule will be applied to the summary widget.
* @constructor
* @param {Object} ruleConfig A JavaScript object representing the configuration of this rule
* @param {Object} domainObject The Summary Widget domain object which contains this rule
* @param {MCT} openmct An MCT instance
* @param {ConditionManager} conditionManager A ConditionManager instance
* @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules
* @param {element} container The DOM element which cotains this summary widget
*/
function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) {
var self = this;
this.config = ruleConfig;
this.domainObject = domainObject;
this.openmct = openmct;
this.conditionManager = conditionManager;
this.widgetDnD = widgetDnD;
this.container = container;
this.domElement = $(ruleTemplate);
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange'];
this.conditions = [];
this.dragging = false;
this.remove = this.remove.bind(this);
this.duplicate = this.duplicate.bind(this);
this.thumbnail = $('.t-widget-thumb', this.domElement);
this.thumbnailLabel = $('.widget-label', this.domElement);
this.title = $('.rule-title', this.domElement);
this.description = $('.rule-description', this.domElement);
this.trigger = $('.t-trigger', this.domElement);
this.toggleConfigButton = $('.view-control', this.domElement);
this.configArea = $('.widget-rule-content', this.domElement);
this.grippy = $('.t-grippy', this.domElement);
this.conditionArea = $('.t-widget-rule-config', this.domElement);
this.jsConditionArea = $('.t-rule-js-condition-input-holder', this.domElement);
this.deleteButton = $('.t-delete', this.domElement);
this.duplicateButton = $('.t-duplicate', this.domElement);
this.addConditionButton = $('.add-condition', this.domElement);
/**
* The text inputs for this rule: any input included in this object will
* have the appropriate event handlers registered to it, and it's corresponding
* field in the domain object will be updated with its value
*/
this.textInputs = {
name: $('.t-rule-name-input', this.domElement),
label: $('.t-rule-label-input', this.domElement),
message: $('.t-rule-message-input', this.domElement),
jsCondition: $('.t-rule-js-condition-input', this.domElement)
};
this.iconInput = new IconPalette('', container);
this.colorInputs = {
'background-color': new ColorPalette('icon-paint-bucket', container),
'border-color': new ColorPalette('icon-line-horz', container),
'color': new ColorPalette('icon-T', container)
};
this.colorInputs.color.toggleNullOption();
/**
* An onchange event handler method for this rule's icon palettes
* @param {string} icon The css class name corresponding to this icon
* @private
*/
function onIconInput(icon) {
self.config.icon = icon;
self.updateDomainObject('icon', icon);
self.thumbnailLabel.removeClass().addClass('label widget-label ' + icon);
self.eventEmitter.emit('change');
}
/**
* An onchange event handler method for this rule's color palettes palettes
* @param {string} color The color selected in the palette
* @param {string} property The css property which this color corresponds to
* @private
*/
function onColorInput(color, property) {
self.config.style[property] = color;
self.updateDomainObject();
self.thumbnail.css(property, color);
self.eventEmitter.emit('change');
}
/**
* An onchange event handler method for this rule's trigger key
* @param {event} event The change event from this rule's select element
* @private
*/
function onTriggerInput(event) {
var elem = event.target;
self.config.trigger = elem.value;
self.generateDescription();
self.updateDomainObject();
self.refreshConditions();
self.eventEmitter.emit('conditionChange');
}
/**
* An onchange event handler method for this rule's text inputs
* @param {element} elem The input element that generated the event
* @param {string} inputKey The field of this rule's configuration to update
* @private
*/
function onTextInput(elem, inputKey) {
self.config[inputKey] = elem.value;
self.updateDomainObject();
if (inputKey === 'name') {
self.title.html(elem.value);
} else if (inputKey === 'label') {
self.thumbnailLabel.html(elem.value);
}
self.eventEmitter.emit('change');
}
/**
* An onchange event handler for a mousedown event that initiates a drag gesture
* @param {event} event A mouseup event that was registered on this rule's grippy
* @private
*/
function onDragStart(event) {
$('.t-drag-indicator').each(function () {
$(this).html($('.widget-rule-header', self.domElement).clone().get(0));
});
self.widgetDnD.setDragImage($('.widget-rule-header', self.domElement).clone().get(0));
self.widgetDnD.dragStart(self.config.id);
self.domElement.hide();
}
/**
* Show or hide this rule's configuration properties
* @private
*/
function toggleConfig() {
self.configArea.toggleClass('expanded');
self.toggleConfigButton.toggleClass('expanded');
self.config.expanded = !self.config.expanded;
}
$('.t-rule-label-input', this.domElement).before(this.iconInput.getDOM());
this.iconInput.set(self.config.icon);
this.iconInput.on('change', function (value) {
onIconInput(value);
});
// Initialize thumbs when first loading
this.thumbnailLabel.removeClass().addClass('label widget-label ' + self.config.icon);
this.thumbnailLabel.html(self.config.label);
Object.keys(this.colorInputs).forEach(function (inputKey) {
var input = self.colorInputs[inputKey];
input.on('change', function (value) {
onColorInput(value, inputKey);
});
input.set(self.config.style[inputKey]);
$('.t-style-input', self.domElement).append(input.getDOM());
});
Object.keys(this.textInputs).forEach(function (inputKey) {
self.textInputs[inputKey].prop('value', self.config[inputKey] || '');
self.textInputs[inputKey].on('input', function () {
onTextInput(this, inputKey);
});
});
this.deleteButton.on('click', this.remove);
this.duplicateButton.on('click', this.duplicate);
this.addConditionButton.on('click', function () {
self.initCondition();
});
this.toggleConfigButton.on('click', toggleConfig);
this.trigger.on('change', onTriggerInput);
this.title.html(self.config.name);
this.description.html(self.config.description);
this.trigger.prop('value', self.config.trigger);
this.grippy.on('mousedown', onDragStart);
this.widgetDnD.on('drop', function () {
this.domElement.show();
$('.t-drag-indicator').hide();
}, this);
if (!this.conditionManager.loadCompleted()) {
this.config.expanded = false;
}
if (!this.config.expanded) {
this.configArea.removeClass('expanded');
this.toggleConfigButton.removeClass('expanded');
}
if (this.domainObject.configuration.ruleOrder.length === 2) {
$('.t-grippy', this.domElement).hide();
}
this.refreshConditions();
//if this is the default rule, hide elements that don't apply
if (this.config.id === 'default') {
$('.t-delete', this.domElement).hide();
$('.t-widget-rule-config', this.domElement).hide();
$('.t-grippy', this.domElement).hide();
}
}
/**
* Return the DOM element representing this rule
* @return {Element} A DOM element
*/
Rule.prototype.getDOM = function () {
return this.domElement;
};
/**
* Unregister any event handlers registered with external sources
*/
Rule.prototype.destroy = function () {
Object.values(this.colorInputs).forEach(function (palette) {
palette.destroy();
});
this.iconInput.destroy();
};
/**
* Register a callback with this rule: supported callbacks are remove, change,
* conditionChange, and duplicate
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will envoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
Rule.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
}
};
/**
* An event handler for when a condition's configuration is modified
* @param {} value
* @param {string} property The path in the configuration to updateDomainObject
* @param {number} index The index of the condition that initiated this change
*/
Rule.prototype.onConditionChange = function (event) {
_.set(this.config.conditions[event.index], event.property, event.value);
this.generateDescription();
this.updateDomainObject();
this.eventEmitter.emit('conditionChange');
};
/**
* During a rule drag event, show the placeholder element after this rule
*/
Rule.prototype.showDragIndicator = function () {
$('.t-drag-indicator').hide();
$('.t-drag-indicator', this.domElement).show();
};
/**
* Mutate thet domain object with this rule's local configuration
*/
Rule.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById.' +
this.config.id, this.config);
};
/**
* Get a property of this rule by key
* @param {string} prop They property key of this rule to get
* @return {} The queried property
*/
Rule.prototype.getProperty = function (prop) {
return this.config[prop];
};
/**
* Remove this rule from the domain object's configuration and invoke any
* registered remove callbacks
*/
Rule.prototype.remove = function () {
var ruleOrder = this.domainObject.configuration.ruleOrder,
ruleConfigById = this.domainObject.configuration.ruleConfigById,
self = this;
ruleConfigById[self.config.id] = undefined;
_.remove(ruleOrder, function (ruleId) {
return ruleId === self.config.id;
});
this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById);
this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder);
this.destroy();
this.eventEmitter.emit('remove');
};
/**
* Makes a deep clone of this rule's configuration, and calls the duplicate event
* callback with the cloned configuration as an argument if one has been registered
*/
Rule.prototype.duplicate = function () {
var sourceRule = JSON.parse(JSON.stringify(this.config));
sourceRule.expanded = true;
this.eventEmitter.emit('duplicate', sourceRule);
};
/**
* Initialze a new condition. If called with the sourceConfig and sourceIndex arguments,
* will insert a new condition with the provided configuration after the sourceIndex
* index. Otherwise, initializes a new blank rule and inserts it at the end
* of the list.
* @param {Object} [config] The configuration to initialize this rule from,
* consisting of sourceCondition and index fields
*/
Rule.prototype.initCondition = function (config) {
var ruleConfigById = this.domainObject.configuration.ruleConfigById,
newConfig,
sourceIndex = config && config.index,
defaultConfig = {
object: '',
key: '',
operation: '',
values: []
};
newConfig = (config !== undefined ? config.sourceCondition : defaultConfig);
if (sourceIndex !== undefined) {
ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig);
} else {
ruleConfigById[this.config.id].conditions.push(newConfig);
}
this.domainObject.configuration.ruleConfigById = ruleConfigById;
this.updateDomainObject();
this.refreshConditions();
};
/**
* Build {Condition} objects from configuration and rebuild associated view
*/
Rule.prototype.refreshConditions = function () {
var self = this;
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) {
$('li:last-of-type', self.conditionArea).before(condition.getDOM());
});
}
if (self.conditions.length === 1) {
self.conditions[0].hideButtons();
}
self.generateDescription();
};
/**
* Remove a condition from this rule's configuration at the given index
* @param {number} removeIndex The index of the condition to remove
*/
Rule.prototype.removeCondition = function (removeIndex) {
var ruleConfigById = this.domainObject.configuration.ruleConfigById,
conditions = ruleConfigById[this.config.id].conditions;
_.remove(conditions, function (condition, index) {
return index === removeIndex;
});
this.domainObject.configuration.ruleConfigById[this.config.id] = this.config;
this.updateDomainObject();
this.refreshConditions();
this.eventEmitter.emit('conditionChange');
};
/**
* Build a human-readable description from this rule's conditions
*/
Rule.prototype.generateDescription = function () {
var description = '',
manager = this.conditionManager,
evaluator = manager.getEvaluator(),
name,
property,
operation,
self = this;
if (this.config.conditions && this.config.id !== 'default') {
if (self.config.trigger === 'js') {
description = 'when a custom JavaScript condition evaluates to true';
} else {
this.config.conditions.forEach(function (condition, index) {
name = manager.getObjectName(condition.object);
property = manager.getTelemetryPropertyName(condition.object, condition.key);
operation = evaluator.getOperationDescription(condition.operation, condition.values);
if (name || property || operation) {
description += 'when ' +
(name ? name + '\'s ' : '') +
(property ? property + ' ' : '') +
(operation ? operation + ' ' : '') +
(self.config.trigger === 'any' ? ' OR ' : ' AND ');
}
});
}
}
if (description.endsWith('OR ')) {
description = description.substring(0, description.length - 3);
}
if (description.endsWith('AND ')) {
description = description.substring(0, description.length - 4);
}
description = (description === '' ? this.config.description : description);
this.description.html(description);
this.config.description = description;
this.updateDomainObject();
};
return Rule;
});

View File

@@ -0,0 +1,323 @@
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.editing = false;
this.container = '';
this.editListenerUnsubscribe = $.noop;
this.ruleArea = $('#ruleArea', 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.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.addRule = this.addRule.bind(this);
this.onEdit = this.onEdit.bind(this);
var id = this.domainObject.identifier.key,
self = this,
oldDomainObject,
statusCapability;
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([]);
}
});
}
/**
* Builds the Summary Widget's DOM, performs other necessary setup, and attaches
* this Summary Widget's view to the supplied container.
* @param {element} container The DOM element that will contain this Summary
* Widget's view.
*/
SummaryWidget.prototype.show = function (container) {
var self = this;
this.container = container;
$(container).append(this.domElement);
$('.widget-test-data', this.domElement).append(this.testDataManager.getDOM());
this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById);
this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
self.initRule(ruleId);
});
this.refreshRules();
this.updateWidget();
this.updateView();
this.addRuleButton.on('click', this.addRule);
this.conditionManager.on('receiveTelemetry', this.executeRules, this);
this.widgetDnD.on('drop', this.reorder, this);
};
/**
* Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry,
* and clean up event handlers
*/
SummaryWidget.prototype.destroy = function (container) {
this.editListenerUnsubscribe();
this.conditionManager.destroy();
this.widgetDnD.destroy();
Object.values(this.rulesById).forEach(function (rule) {
rule.destroy();
});
};
/**
* A callback function for the Open MCT status capability listener. If the
* view representing the domain object is in edit mode, update the internal
* state and widget view accordingly.
* @param {string[]} status an array containing the domain object's current status
*/
SummaryWidget.prototype.onEdit = function (status) {
if (status && status.includes('editing')) {
this.editing = true;
} else {
this.editing = false;
}
this.updateView();
};
/**
* If this view is currently in edit mode, show all rule configuration interfaces.
* Otherwise, hide them.
*/
SummaryWidget.prototype.updateView = function () {
if (this.editing) {
this.ruleArea.show();
this.testDataArea.show();
this.addRuleButton.show();
} else {
this.ruleArea.hide();
this.testDataArea.hide();
this.addRuleButton.hide();
}
};
/**
* Update the view from the current rule configuration and order
*/
SummaryWidget.prototype.refreshRules = function () {
var self = this,
ruleOrder = self.domainObject.configuration.ruleOrder,
rules = self.rulesById;
self.ruleArea.html('');
Object.values(ruleOrder).forEach(function (ruleId) {
self.ruleArea.append(rules[ruleId].getDOM());
});
this.executeRules();
};
/**
* Update the widget's appearance from the configuration of the active rule
*/
SummaryWidget.prototype.updateWidget = function () {
var activeRule = this.rulesById[this.activeId];
this.applyStyle($('#widget', this.domElement), activeRule.getProperty('style'));
$('#widget', this.domElement).prop('title', activeRule.getProperty('message'));
$('#widgetLabel', this.domElement).html(activeRule.getProperty('label'));
$('#widgetLabel', this.domElement).removeClass().addClass('label widget-label ' + activeRule.getProperty('icon'));
};
/**
* Get the active rule and update the Widget's appearance.
*/
SummaryWidget.prototype.executeRules = function () {
this.activeId = this.conditionManager.executeRules(
this.domainObject.configuration.ruleOrder,
this.rulesById
);
this.updateWidget();
};
/**
* Add a new rule to this widget
*/
SummaryWidget.prototype.addRule = function () {
var ruleCount = 0,
ruleId,
ruleOrder = this.domainObject.configuration.ruleOrder;
while (Object.keys(this.rulesById).includes('rule' + ruleCount)) {
ruleCount = ++ruleCount;
}
ruleId = 'rule' + ruleCount;
ruleOrder.push(ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
this.initRule(ruleId, 'Rule');
this.refreshRules();
};
/**
* Duplicate an existing widget rule from its configuration and splice it in
* after the rule it duplicates
* @param {Object} sourceConfig The configuration properties of the rule to be
* instantiated
*/
SummaryWidget.prototype.duplicateRule = function (sourceConfig) {
var ruleCount = 0,
ruleId,
sourceRuleId = sourceConfig.id,
ruleOrder = this.domainObject.configuration.ruleOrder,
ruleIds = Object.keys(this.rulesById);
while (ruleIds.includes('rule' + ruleCount)) {
ruleCount = ++ruleCount;
}
ruleId = 'rule' + ruleCount;
sourceConfig.id = ruleId;
sourceConfig.name += ' Copy';
ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig;
this.updateDomainObject();
this.initRule(ruleId, sourceConfig.name);
this.refreshRules();
};
/**
* Initialze a new rule from a default configuration, or build a {Rule} object
* from it if already exists
* @param {string} ruleId An key to be used to identify this ruleId, or the key
of the rule to be instantiated
* @param {string} ruleName The initial human-readable name of this rule
*/
SummaryWidget.prototype.initRule = function (ruleId, ruleName) {
var ruleConfig,
styleObj = {};
Object.assign(styleObj, DEFAULT_PROPS);
if (!this.domainObject.configuration.ruleConfigById[ruleId]) {
this.domainObject.configuration.ruleConfigById[ruleId] = {
name: ruleName || 'Rule',
label: this.domainObject.name,
message: '',
id: ruleId,
icon: ' ',
style: styleObj,
description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule',
conditions: [{
object: '',
key: '',
operation: '',
values: []
}],
jsCondition: '',
trigger: 'any',
expanded: 'true'
};
}
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct,
this.conditionManager, this.widgetDnD, this.container);
this.rulesById[ruleId].on('remove', this.refreshRules, this);
this.rulesById[ruleId].on('duplicate', this.duplicateRule, this);
this.rulesById[ruleId].on('change', this.updateWidget, this);
this.rulesById[ruleId].on('conditionChange', this.executeRules, this);
};
/**
* Given two ruleIds, move the source rule after the target rule and update
* the view.
* @param {Object} event An event object representing this drop with draggingId
* and dropTarget fields
*/
SummaryWidget.prototype.reorder = function (event) {
var ruleOrder = this.domainObject.configuration.ruleOrder,
sourceIndex = ruleOrder.indexOf(event.draggingId),
targetIndex;
if (event.draggingId !== event.dropTarget) {
ruleOrder.splice(sourceIndex, 1);
targetIndex = ruleOrder.indexOf(event.dropTarget);
ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
}
this.refreshRules();
};
/**
* Apply a list of css properties to an element
* @param {element} elem The DOM element to which the rules will be applied
* @param {object} style an object representing the style
*/
SummaryWidget.prototype.applyStyle = function (elem, style) {
Object.keys(style).forEach(function (propId) {
elem.css(propId, style[propId]);
});
};
/**
* Mutate this domain object's configuration with the current local configuration
*/
SummaryWidget.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration);
};
return SummaryWidget;
});

View 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;
});

View File

@@ -0,0 +1,202 @@
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.configArea = $('.t-widget-test-data-content', this.domElement);
this.itemArea = $('.t-test-data-config', this.domElement);
this.toggleConfigButton = $('.view-control', this.domElement);
this.addItemButton = $('.add-test-condition', this.domElement);
this.testDataInput = $('.t-test-data-checkbox', this.domElement);
/**
* Toggles the configuration area for test data in the view
* @private
*/
function toggleConfig() {
self.configArea.toggleClass('expanded');
self.toggleConfigButton.toggleClass('expanded');
}
/**
* 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.toggleConfigButton.on('click', toggleConfig);
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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View File

@@ -0,0 +1,337 @@
define(['../src/ConditionEvaluator'], function (ConditionEvaluator) {
describe('A Summary Widget Rule Evaluator', function () {
var evaluator,
testEvaluator,
testOperation,
mockCache,
mockTestCache,
mockComposition,
mockConditions,
mockConditionsEmpty,
mockConditionsUndefined,
mockConditionsAnyTrue,
mockConditionsAllTrue,
mockConditionsAnyFalse,
mockConditionsAllFalse,
mockOperations;
beforeEach(function () {
mockCache = {
a: {
alpha: 3,
beta: 9,
gamma: 'Testing 1 2 3'
},
b: {
alpha: 44,
beta: 23,
gamma: 'Hello World'
},
c: {
foo: 'bar',
iAm: 'The Walrus',
creature: {
type: 'Centaur'
}
}
};
mockTestCache = {
a: {
alpha: 1,
beta: 1,
gamma: 'Testing 4 5 6'
},
b: {
alpha: 2,
beta: 2,
gamma: 'Goodbye world'
}
};
mockComposition = {
a: {},
b: {},
c: {}
};
mockConditions = [{
object: 'a',
key: 'alpha',
operation: 'greaterThan',
values: [2]
},{
object: 'b',
key: 'gamma',
operation: 'lessThan',
values: [5]
}];
mockConditionsEmpty = [{
object: '',
key: '',
operation: '',
values: []
}];
mockConditionsUndefined = [{
object: 'No Such Object',
key: '',
operation: '',
values: []
},{
object: 'a',
key: 'No Such Key',
operation: '',
values: []
},{
object: 'a',
key: 'alpha',
operation: 'No Such Operation',
values: []
},{
object: 'all',
key: 'Nonexistent Field',
operation: 'Random Operation',
values: []
},{
object: 'any',
key: 'Nonexistent Field',
operation: 'Whatever Operation',
values: []
}];
mockConditionsAnyTrue = [{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}];
mockConditionsAnyFalse = [{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [1000]
}];
mockConditionsAllFalse = [{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}];
mockConditionsAllTrue = [{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [0]
}];
mockOperations = {
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is Greater Than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values [0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is Less Than',
appliesTo: ['number'],
inputCount: 1
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'Text Contains',
appliesTo: ['string'],
inputCount: 1
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'Text is Exactly',
appliesTo: ['string'],
inputCount: 1
},
isHalfHorse: {
operation: function (input) {
return input[0].type === 'Centaur';
},
text: 'is Half Horse',
appliesTo: ['mythicalCreature'],
inputCount: 0,
getDescription: function () {
return 'is half horse';
}
}
};
evaluator = new ConditionEvaluator(mockCache, mockComposition);
testEvaluator = new ConditionEvaluator(mockCache, mockComposition);
evaluator.operations = mockOperations;
});
it('evaluates a condition when it has no configuration', function () {
expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false);
expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false);
});
it('correctly evaluates a set of conditions', function () {
expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
});
it('correctly evaluates conditions involving "any telemetry"', function () {
expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true);
expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false);
});
it('correctly evaluates conditions involving "all telemetry"', function () {
expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true);
expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false);
});
it('handles malformed conditions gracefully', function () {
//if no conditions are fully defined, should return false for any mode
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false);
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false);
expect(evaluator.execute(mockConditionsUndefined, 'js')).toEqual(false);
//these conditions are true: evaluator should ignore undefined conditions,
//and evaluate the rule as true
mockConditionsUndefined.push({
object: 'a',
key: 'gamma',
operation: 'textContains',
values: ['Testing']
});
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true);
mockConditionsUndefined.push({
object: 'c',
key: 'iAm',
operation: 'textContains',
values: ['Walrus']
});
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true);
});
it('gets the keys for possible operations', function () {
expect(evaluator.getOperationKeys()).toEqual(
['greaterThan', 'lessThan', 'textContains', 'textIsExactly', 'isHalfHorse']
);
});
it('gets output text for a given operation', function () {
expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse');
});
it('correctly returns whether an operation applies to a given type', function () {
expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true);
expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false);
});
it('returns the HTML input type associated with a given data type', function () {
expect(evaluator.getInputTypeById('string')).toEqual('text');
});
it('gets the number of inputs required for a given operation', function () {
expect(evaluator.getInputCount('isHalfHorse')).toEqual(0);
expect(evaluator.getInputCount('greaterThan')).toEqual(1);
});
it('gets a human-readable description of a condition', function () {
expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse');
expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1');
});
it('allows setting a substitute cache for testing purposes, and toggling its use', function () {
evaluator.setTestDataCache(mockTestCache);
evaluator.useTestData(true);
expect(evaluator.execute(mockConditions, 'any')).toEqual(false);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
mockConditions.push({
object: 'a',
key: 'gamma',
operation: 'textContains',
values: ['4 5 6']
});
expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
mockConditions.pop();
evaluator.useTestData(false);
expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
});
it('supports all required operations', function () {
//equal to
testOperation = testEvaluator.operations.equalTo.operation;
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(false);
//not equal to
testOperation = testEvaluator.operations.notEqualTo.operation;
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than
testOperation = testEvaluator.operations.greaterThan.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(false);
//less than
testOperation = testEvaluator.operations.lessThan.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than or equal to
testOperation = testEvaluator.operations.greaterThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(false);
//less than or equal to
testOperation = testEvaluator.operations.lessThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(true);
//between
testOperation = testEvaluator.operations.between.operation;
expect(testOperation([100, 33, 66])).toEqual(false);
expect(testOperation([1, 33, 66])).toEqual(false);
expect(testOperation([45, 33, 66])).toEqual(true);
//not between
testOperation = testEvaluator.operations.notBetween.operation;
expect(testOperation([100, 33, 66])).toEqual(true);
expect(testOperation([1, 33, 66])).toEqual(true);
expect(testOperation([45, 33, 66])).toEqual(false);
//text contains
testOperation = testEvaluator.operations.textContains.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(true);
expect(testOperation(['Testing', 'bind'])).toEqual(false);
//text does not contain
testOperation = testEvaluator.operations.textDoesNotContain.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(false);
expect(testOperation(['Testing', 'bind'])).toEqual(true);
//text starts with
testOperation = testEvaluator.operations.textStartsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(true);
expect(testOperation(['Testing', 'ting'])).toEqual(false);
//text ends with
testOperation = testEvaluator.operations.textEndsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(false);
expect(testOperation(['Testing', 'ting'])).toEqual(true);
//text is exactly
testOperation = testEvaluator.operations.textIsExactly.operation;
expect(testOperation(['Testing', 'Testing'])).toEqual(true);
expect(testOperation(['Testing', 'Test'])).toEqual(false);
//undefined
testOperation = testEvaluator.operations.isUndefined.operation;
expect(testOperation([1])).toEqual(false);
expect(testOperation([])).toEqual(true);
});
it('can produce a description for all supported operations', function () {
testEvaluator.getOperationKeys().forEach(function (key) {
expect(testEvaluator.getOperationDescription(key, [])).toBeDefined();
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More