Compare commits

..

190 Commits

Author SHA1 Message Date
4fb0dac528 remove check for editing when persisting, in SummaryWidget.js 2017-11-28 10:32:40 -08:00
c863ab42a6 add request properties to telemetry request for providers that support it 2017-11-28 10:18:37 -08:00
81d349b7d5 Throw error when unsupported callback is passed in condition.js, return summary widget instance in plugin.js instead of wrapping in new object for view 2017-11-28 09:30:48 -08:00
b18c09fab2 fix checkstyle error 2017-11-26 09:23:29 -08:00
9efc525395 make changes requested by reviewer - remove generateDescription from refreshConditions and add it after refreshConditions to initCondition and deleteCondition 2017-11-26 09:16:29 -08:00
1f4a3f4593 merge from latest summary widgets 2017-11-26 09:01:04 -08:00
00f4dcd558 fix plugin.js file to include summary widgets 2017-11-24 12:31:36 -08:00
0bc05e1b9e update with master 2017-11-24 12:11:51 -08:00
5a49ac16b1 Merge pull request #1417 from ev1stensberg/URLIndicator
[BUILD] Adds URLIndicator
2017-11-24 10:17:04 -08:00
91b150c064 icon -> cssClass 2017-11-24 10:04:59 -08:00
9506d309b0 [TEST] Add Unit Test for URLIndicator
Adds the first test for the URLIndicator.

[DOCS] fix docs api indenting

fix linting

fix linting

fix docs
2017-11-24 10:04:59 -08:00
c9bd60f50e [BUILD] Adds URLIndicator along with documentation
Adds URLIndicator to the build, testable adding

`openmct.install(new openmct.plugins.URLIndicatorPlugin({
url: 'http://localhost:8080/',
icon: 'check',
interval: 15000,
label: 'Localhost'
}))`

to the openmct file. Also added Documentation about the plugin.
2017-11-24 10:04:59 -08:00
cf15ff5c07 [imagery] fix for issue #1799 (#1814)
* [imagery] fix for issue #1799
if there is no immediately new imagery incoming, forward to latest image in history on un-pause

* add test to check whether image is forwarded to latest image on un-pause
2017-11-22 11:10:53 -08:00
6bbdfcdfbe [Table] Retain rows in scope (#1813)
* [Table] Push rows in as they are added

...such that sorting does not cause real-time table rows to be lost.
Fixes #1738

* [Table] Test adding rows

* [Table] Fix code style in test

https://github.com/nasa/openmct/pull/1813#pullrequestreview-78277635
2017-11-22 11:05:03 -08:00
06e93ff520 Merge pull request #1721 from nasa/persistence-issue-1593
[persistence] uncaught in promise error fix for issue #1593
2017-11-21 14:14:35 -08:00
550e7a15e6 [persistence] fix for issue #1593
prevent EditorCapability#finish from calling transactionservice#cancel when transactionService was not active - leading to console error everytime user would leave edit mode

Add tests for changes made and also to check for return type in either case (isActive or not)
2017-11-21 12:07:59 -08:00
c931c54332 add key property to view provider to comply with new View API 2017-11-20 14:43:42 -08:00
4ec6f797e7 merge with master 2017-11-20 14:34:43 -08:00
c40cc3e060 pull from master 2017-11-20 14:21:18 -08:00
7ee4a508f6 squash commits 2017-11-20 14:16:48 -08:00
28da6a5106 prevent persist on initialize by checking if in edit mode, and add watch for changes unsubscribe on destroy
Remove unnecessary persist calls in Rule.js.
2017-11-20 13:48:22 -08:00
a4f203332a [Summary Widget] Added margin to the delete button (#1762) 2017-11-20 09:42:12 -08:00
b9a678bf9a Fix grippy bug, where grippies arent correctly showing up when rules go down from 3 to 2.
Prevent HTML injection in summary widget labels.
Change default name of summary widget to 'Unnamed Rule' to avoid confusion.
2017-11-20 09:39:29 -08:00
71c54cd541 Merge pull request #1794 from nasa/open599b
[Timeline] Add resource graphs by drag-and-drop
2017-11-17 16:44:59 -08:00
e81b8e53dc Merge pull request #1797 from nasa/object-provider-fix
[API] get with keystring, fix transitional race condition
2017-11-17 09:00:00 -08:00
82a35cb721 changes requested by reviewer + find and fix causes of unnecessary persist calls in Rule.js 2017-11-15 11:07:11 -08:00
84e6928f54 Merge pull request #1809 from nasa/file-saver-1808
[Build] Update file-saver dependency
2017-11-14 16:08:08 -08:00
ba688fe62c [Build] Update file-saver dependency
Fixes #1808
2017-11-14 14:27:43 -08:00
5c93798832 prevent persist on initialize by checkingif in edit mode, and add watch for changes unsubscribe on destroy 2017-11-03 11:33:30 -07:00
c533e10352 [API] get with keystring, mark methods not implemented
Update Object API such that get supports calls with either a keystring or an
identifier.

As save and delete are not implemented and  have different calling signatures,
implement them as separate methods so they can be documented separately.
2017-11-01 12:16:06 -07:00
04f47b3db6 [Timeline] Test MCTResourceGraphDrop's dragleave handler 2017-10-30 12:44:51 -07:00
717fa5edf4 [Timeline] Test MCTResourceGraphDrop's drop handler 2017-10-30 12:42:18 -07:00
14f5f048fb [Timeline] Test MCTResourceGraphDrop's dragover handler 2017-10-30 12:40:08 -07:00
72929500d3 [Timeline] Begin adding spec for MCTResourceGraphDrop
...to follow up on PR #1195, which fixes #599
2017-10-30 12:25:59 -07:00
471adde923 [Swimlanes] Check for valid swimlane
Issue #599. Also switches class toggle from scope based to element
based.
2017-10-30 12:07:52 -07:00
6c5d5f3d00 [Swimlanes] Implement resource graph directive
Issue #599
2017-10-30 12:07:52 -07:00
2262fef29b [Swimlanes] Add resource graph drop directive
Issue #599
2017-10-30 12:07:49 -07:00
bda30f1475 Merge pull request #1787 from nasa/fixed-position-panels
[Fixed Position] Fixed position displays now show image URLs
2017-10-30 11:31:37 -07:00
bfec434369 Merge pull request #1793 from nasa/layout-regression-1790
[Layout] Fix regression in selecting object when it's dropped
2017-10-30 09:43:49 -07:00
14df350994 [Layout] Fix regression...
...by clearing the selection only if the selected object is no longer in the compositon and there's no newly dropped object.

Fixes # 1790
2017-10-27 18:18:09 -07:00
80582f5e8d [Fixed Position] Do not just show range values, be more flexible to other telemetry types. Fixes #1740 2017-10-24 11:44:06 -07:00
7442768ced [List] Use standard format for modified/persisted times (#1737)
* [List] Use standard format for modified/persisted times

This provides consistency with other times and dates in the user interface,
and also provides a meaningful sort order due to the use of ISO formats for
standard date/time presentation. Fixes #1730.

* Remove unused dependency
2017-10-20 18:25:49 -07:00
77c7bdfdec [Timers] Follow timers from timelines (#1694)
* Squashed commit of the following:

commit f1dc1ce152e186da0d10c8e77d920ac0a76c9bc2
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:35:38 2017 -0700

    [Timers] Rewrite JSDoc for FollowTimerAction

    https://github.com/nasa/openmct/pull/1694/files#r137604769

commit 7ab0693cc983f8a04ac8ee9002f4d776b06a869a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:27:53 2017 -0700

    [Timer] Expect domain objects from FollowIndicator test

commit ff89c0849d16ab451bfd2fddd9202cf36940f599
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:26:28 2017 -0700

    [Timer] Add JSDoc for new method

commit 2a0343352eca241dfc28a4aa0b3832e3e6928864
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:24:59 2017 -0700

    [Timeline] Update TOI tests

    ...to account for refactoring out of tick handling.

commit 01cbaafc72870fab4ada5894637ae5721214933d
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:17:25 2017 -0700

    [Timeline] Update dependencies for TOI test

commit 6bd5c378566362dce331e7c200dea87f0b08ecc6
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:15:21 2017 -0700

    [Timers] Update timerService tests with dependencies

commit b0793865c5131e17a58786ec356d67f2f2bba4c5
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:09:54 2017 -0700

    [Timers] Declare vars to satisfy JSHint

commit 9d2a63f7fe61dadf68255d795512ec55f532c533
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 14:07:12 2017 -0700

    [Timeline] Handle stopped timer

commit 30871270514730f3f2f12482075e5140bb97fa1f
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 13:59:08 2017 -0700

    [Timer] Tweak refactored timer logic

commit 53ad127ba7cf679377dc865301612a1d78399324
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 13:53:36 2017 -0700

    [Timer] Convert times from timerService

    ...to reduce resposibilities for TOI controller.

commit f8341133cf23df383b8f6e4815b88e0066ebd2bc
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 13:03:37 2017 -0700

    [Timeline] Factor out timer knowledge

commit aebd9e0ac223971b868b03343dbe4c61c6eb4849
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 12:34:58 2017 -0700

    [Timeline] Consistently use this

commit 48ac427a20c5c343aecdbd54b068d8691f7830b6
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 12:33:57 2017 -0700

    [Timeline] Remove unused tick binding/call

commit ea62f0a15ba4ab5de53213bbed14599eaf878d70
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 12:10:59 2017 -0700

    [Timeline] Retrieve timestamp on demand

commit f53bd04b5e343b22ea52b431785ade891577bb6a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 12:07:55 2017 -0700

    [Timeline] Update clocks on bounds events

    https://github.com/nasa/openmct/pull/1694/files#r137603081

commit 51d8e376ee46aafa13cd9a969c6f03885e10dafb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 11:40:33 2017 -0700

    [Timeline] Don't listen for non-existing tick events

commit 5cc40c488cec5e7453c2fe1dea5e5a4fa3509ecd
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 11:39:21 2017 -0700

    [Time] Revert Time API changes

    https://github.com/nasa/openmct/pull/1694/files#r137603081

commit c55c8bc627bf0a7f3cfd04b604b82d15ff469ab9
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 11:37:40 2017 -0700

    [Timeline] Finish testing TOI controller

commit af5cea5f2f172a309568d477dfdf11b8d45e74bb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 11:06:47 2017 -0700

    [Timeline] Test TOI controller

commit ba64db68b132fa431e8ccdb533024bf2850f9712
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 10:06:41 2017 -0700

    [Timers] Test timerService

commit 247e663b326ec5b8145b832af9b26086204baea3
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 10:05:24 2017 -0700

    [Timers] Remove unused timerService method

commit 8d741ad5744e1b7deb669dbaa0f3d30e4eb5866e
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 09:59:32 2017 -0700

    [Timers] Remove unused timerService dependency

commit b59c8917bdef5ec3e54c8857d993d86547cfe177
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 09:58:10 2017 -0700

    [Timers] Remove unused timerService event

commit f15dd9827f835a814dc40a6201c90268a60ed64a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 09:49:09 2017 -0700

    [Timers] Test timer-following indicator

commit 2501f11af8c0b2aed9ebf16ffd28c0003b2701c2
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 09:42:54 2017 -0700

    [Timer] Complete test coverage for FollowTimerAction

commit aa2be83fc15cd68ee6de4d9f8205dc2fcba8c35b
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 7 09:35:37 2017 -0700

    [Timers] Begin testing Follow Timer action

commit d9062e0b0ff351b141dcb646972053ec72292d53
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:45:18 2017 -0700

    [Timeline] Remove unused variables

commit 79ebe4dd2b2aefc1e83ea8142588ed0715b3c269
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:39:22 2017 -0700

    [Timeline] JSDoc for TOI controller

commit 330f6b465188555e8e59f4eaf8ce1875b5335846
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:30:58 2017 -0700

    [Timeline] Use different icon to follow time bounds

commit f0a3b628e6d1d843324085edd563b68997f5a215
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:30:46 2017 -0700

    [Timeline] Simplify TOI following initialization

commit e76f3d1d525e0d19845b4c5b457995e60c416ad0
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:27:07 2017 -0700

    [Timeline] Add toggle to follow time bounds

commit 8ec072c0a2a953c074e0c327430dd68f27894ffb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 16:19:25 2017 -0700

    [Timeline] Follow TC bounds based on boolean

commit 206a26734dedc267af6d298a77658aa261ca4fea
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 15:37:12 2017 -0700

    [Timeline] Tune bounds following

commit 19563bdf53a036c7bf09c52924425a1902b243bb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 15:19:19 2017 -0700

    [Timeline] Remove unused method

commit 293981ec55ad115d7bd90b92f5bd090df64bd7c2
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 15:18:59 2017 -0700

    [Timeline] Only update timestamp on tick

    Leave bounds-following to the bounds event

commit 9180e15971d2043f0999a16f0aa8794273bcfc74
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 14:43:01 2017 -0700

    [Time] Document tick event

commit c7b163dff0d94aaea86b76647501f21623b353e1
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 14:39:57 2017 -0700

    [Timeline] Stop listening on destroy

    ...from the TOI controller.

commit ca7def3cf98e1eaf6c3aeb16cf9fd79452c86bd0
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 14:32:40 2017 -0700

    [Timeline] Remove surplus watches

commit 367e7afa94ae1ed448e39f13be804c62e2bfcf00
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 14:30:14 2017 -0700

    [Timeline] Very deltas are valid before panning

commit 7ee94f316e90d046015266a2a9168e349ff73345
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 14:28:10 2017 -0700

    [Timeline] Scroll with TOI only while in view

commit 9d7bb431119b7bc6ddc86f0058718e4385478518
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 10:36:46 2017 -0700

    [Timeline] Utilize zoomController.bounds

commit f151b9e8adfd235c31e32bef5fddab835efa7c8f
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 10:35:57 2017 -0700

    [Timeline] Add methods to set zoom bounds

commit c3d0b9876ab79c18003838ed3315045c5fb2ddbb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 10:32:08 2017 -0700

    [Timelines] Observe bounds changes

    ...to synchronize zoom with Time Conductor, #1688

commit 58adafc46f231b0fd92827d10c377131166ff39c
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 09:37:07 2017 -0700

    [Timers] JSDoc for TimerService

commit a325a8d5085bf1a4c9aa3ab20771308d4789765a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 09:12:50 2017 -0700

    [Timeline] Re-tweak follow scroll calculations

    ...for visibility.

commit 41e4bf153607b081aaf92253fa2b21300e2f0ea7
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 09:03:45 2017 -0700

    [Timeline] Tweak follow scroll calculations

    ...for visibility.

commit 08a5b9f14ab629a310dc27a3771ca454f1187327
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 08:59:45 2017 -0700

    [Timeline] Replace debug output with scroll updates

commit 26585ecd61341b4ee89abd8ee866e705a02bbc9a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 08:59:07 2017 -0700

    [Timeline] Move TOI to scrollable area

commit 654eda027c3c67a3a0ff33136109ca27d14762ba
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 08:56:07 2017 -0700

    [Timeline] Begin implementing TOI following

commit 552f67a11ce439be58ab7ca7884c46241a25adee
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Sep 6 08:55:51 2017 -0700

    [Timeline] At zoom-to-time method

    For use by time-of-interest controller, #1688

commit 37acbfd458740b2c3176875f83d37f0fdf57e727
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 12:46:33 2017 -0700

    [Timeline] Remove other excess $apply calls

    ...although this should make us nervy about those callbacks being
    invoked in different ways.

commit 0e72847c9ba59f957efa2d412cb77c024afa9e63
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 12:44:27 2017 -0700

    [Timeline] Remove $apply from $watch callback

    ...to avoid an infinite digest loop.

commit bade0fd9f60101d5b1b782cd28e608af493c9076
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 12:42:18 2017 -0700

    [Timeline] Begin adding TOI line to template

commit f94034a3b4136f6b174155397084f8cdb22ce544
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 12:11:25 2017 -0700

    [Timers] Add missing semicolon, satisfy JSHint

commit cb465b94011e7432cc7e4d9e815641f97dc61d7a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 12:08:45 2017 -0700

    [Time] Verify that tick event is emitted

commit 7c84a86a33ceb73ba6a06801374ea3f89793c450
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 11:59:06 2017 -0700

    [Time] Emit tick events from Time API

    https://github.com/nasa/openmct/pull/1694

commit d319a783fcd882c03eb7d9a81fec33898016384e
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Wed Aug 30 11:56:28 2017 -0700

    [Timeline] Sketch in TOI controller

    ...to position/follow time-of-interest, relative to the active timer.

commit 2dbdb2627450039d69dbfd10eed2c100207e061a
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:57:47 2017 -0700

    [Timers] Use timerService

    ...to coordinate between action and indicator

commit f94a2358eaf0366bd4da2b44e69ccb62b153c5db
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:52:22 2017 -0700

    [Timers] Use TimerService from Follow Timer action

commit a720c2ec2cda4a300d26167f4717f0571bedcbfd
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:50:31 2017 -0700

    [Timers] Expose TimerService through bundle

commit e32bbc3e232d25f7c5dba98674781e4f263c4870
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:49:03 2017 -0700

    [Timers] Sketch in timer service

    ...which will keep track of the active timer used to interpret SET
    for Timelines.

commit a038c2b1d8fd34c2874fa8fc0421fa7ba53e11ab
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:41:05 2017 -0700

    [Timers] Register indicator

commit 0e93ae87a1cccc4f3a0636844625b64ccb77a7ae
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 12:39:21 2017 -0700

    [Timers] Skeleton for time following indicator

commit e806386891639740e9fe3d8641c2f60ab5a88eac
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 09:37:14 2017 -0700

    [Timers] Register the Follow Timer action

commit 008aa95932070459dcc6fa1d918a23dac8df7592
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Tue Aug 29 09:35:08 2017 -0700

    [Timers] Skeleton for Follow Timer action

    ...to synchronize the time conductor with a particular Timer. #1688

* [Timers] Remove unused variable to pass lint checks

* [Timers] Frontend updates for time-of-interest

Squashed commit of the following:

commit 370b910d36
Author: Charles Hacskaylo <charlesh88@gmail.com>
Date:   Wed Sep 20 10:59:00 2017 -0700

    [Frontend] Fix in FollowIndicator.js

    Fixes #1688

commit 883d1feb32
Author: Charles Hacskaylo <charlesh88@gmail.com>
Date:   Wed Sep 20 10:36:56 2017 -0700

    [Frontend] Styling and content on Follow indicator

    Fixes #1688

commit cff85fbbde
Author: Charles Hacskaylo <charlesh88@gmail.com>
Date:   Wed Sep 20 10:09:19 2017 -0700

    [Frontend] Styling complete on Follow Line

    Fixes #1688

commit 563a86b69f
Author: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Date:   Mon Sep 18 16:05:53 2017 -0700

    [Front-end] WIP Markup and CSS for Follow Line

    Fixes #1688
    Added line icon, style refinement;

commit fc49e5d023
Author: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Date:   Mon Sep 18 15:07:35 2017 -0700

    [Front-end] WIP Markup and CSS for Follow Line

    Fixes #1688
    Moved TimelineTOIController up 2 levels of markup hierarchy
    to allow Follow Lines, one in each split pane;
    Follow LInes markup and CSS in progress;

commit 8ec3c42291
Author: Charles Hacskaylo <charlesh88@gmail.com>
Date:   Wed Sep 13 16:46:14 2017 -0700

    [Frontend] WIP Timeline Follow Line

    Fixes #1688
    VERY WIP! Initial move of styles into classes;

* [Timeline] Follow up on front-end updates

Fixes #1688

Squashed commit of the following:

commit 817c7f31289b3e7631c3332d2192a68f21f50f9e
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 21 12:47:48 2017 -0700

    [Timeline] Initialize lastWidth

    ...to avoid clamping values before a width has actually been observed.

commit 5f7324c1cdb0cbef6385fbccac31b0404d216f95
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 21 12:21:11 2017 -0700

    [Timeline] Clamp right edge of zoom

    ...to avoid getting stuck in a weird scrolling state for large
    timer values.

commit 076aca112392e65835e7a01ac8e28780d24bfff1
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 21 12:02:23 2017 -0700

    [Timeline] Don't set scroll.x to negative values

    ...avoids mispositioning timer-following line,
    https://github.com/nasa/openmct/issues/1688#issuecomment-330373625

commit ac9bdb919df69fac65b297487131e2c41204ebeb
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Sep 21 11:32:49 2017 -0700

    [Timers] Loosen test expectation

    Resolves build failure https://circleci.com/gh/nasa/openmct/4181
    by reducing test specificity for indicator display name.

* [Timer] Handle mutations to followed timers

Fixes #1741

Squashed commit of the following:

commit 5fdd156dc9089baac2e975a85373146e0b788731
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Oct 5 12:18:06 2017 -0700

    [Timer] Test mutation observation

    ...to verify resolution of root cause for #1741

commit 348b193fd45fc457d4b56bc1ddb2249aab65afba
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Oct 5 12:15:05 2017 -0700

    [Timers] Update expected API usage in Follow Timer test

commit 7a584dd993d68c4c50a99ac66976420b5931893c
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Oct 5 12:12:11 2017 -0700

    [Timers] Update spec for timerService

    ...to account for use of openmct.objects

commit ad396a79f0bad9dfc5382745943dd34ddcee1bef
Author: Victor Woeltjen <victor.woeltjen@nasa.gov>
Date:   Thu Oct 5 12:10:25 2017 -0700

    [Timer] Observe timer mutations

    ...such that followed timer remains in sync with timer model,
    e.g. during navigation. Fixes #1741
2017-10-20 18:05:35 -07:00
07d9769966 Merge pull request #1786 from nasa/pass-row-as-structure-1785
[Controls] pass item as structure
2017-10-20 17:29:48 -07:00
385b6177b2 [Controls] pass item as structure
Pass the item to child controls inside of a composite instead of
the row object.   Thus, options are correctly passed to children.

Fixes #1785.
2017-10-20 17:19:11 -07:00
7f68d26433 Merge pull request #1642 from nasa/view-api-implementation
[ViewAPI] implement initial view API
2017-10-20 17:11:28 -07:00
e7e4c2f704 Merge pull request #1768 from savva-k/button-distance-1762
[Summary Widget] Added margin to the delete button (#1762)
2017-10-12 09:33:21 -07:00
e6beaf299f change default label to 'Unnamed Rule' to avoid confusion 2017-10-11 11:08:09 -07:00
8bc3621766 fix bug regarding grippy persisting on rule0 even if there are only 2 rules left, includes some code cleanup 2017-10-10 10:33:29 -07:00
d7b8cd1365 [Summary Widget] Added margin to the delete button (#1762) 2017-10-09 21:54:36 +03:00
64a9a60eae prevent html injection in rule labels 2017-10-05 16:39:59 -07:00
1f4be812e5 fix checkstyle error 2017-10-05 10:10:09 -07:00
156d84b303 fix grippy bug 2017-10-05 10:04:45 -07:00
971eda4d88 [Frontend] Standardized font-size in widgets
Fixes #1668
2017-10-04 09:23:10 -07:00
68e6e3c121 finalize changes for v1 and add/fix tests 2017-10-02 09:32:26 -07:00
7e7f39db2d Add tooltip values for duplicate and delete buttons
Fixes #1668
2017-09-28 15:36:47 -07:00
b6e0fca828 Standardize to sentence casing
Fixes #1668
2017-09-28 15:31:26 -07:00
ffc5896e5a Merge branch 'summary-widgets' of https://github.com/nasa/openmct into summary-widgets 2017-09-28 11:06:06 -07:00
fd6ebd152f fix and add appropriate tests 2017-09-28 11:05:42 -07:00
7a5c1c0e1f Clean up commented code
Fixes #1668
2017-09-28 10:35:38 -07:00
2f7e1e3f1a [Frontend] Added missing condition label text
Fixes #1668
Each condition in a rule beyond the first one
now displays the text label "or when" or "and when"
depending on the value of the user's selection in
the "Trigger when" control: "any" or "all"
2017-09-27 15:53:43 -07:00
d73746b51b [Frontend] Fix for tooltip not appearing
Fixes #1668
2017-09-27 14:49:00 -07:00
2df54af019 Merge branch 'summary-widgets' of https://github.com/nasa/openmct into summary-widgets 2017-09-27 11:23:57 -07:00
586269f761 remove js option from rules 2017-09-27 11:23:47 -07:00
e536ab34d7 [Frontend] Added l-flex-accordion class
Fixes #1668
2017-09-26 17:44:35 -07:00
e15002dd72 [Frontend] Added Rules expand/contract
Fixes #1668
2017-09-26 17:43:53 -07:00
453cf3ad6a watch for object changes and update url and newTab properties 2017-09-26 14:18:09 -07:00
5c46e48bde Merge lastest master, resolve conflicts in _menus.scss
Fixes #1668
2017-09-26 11:51:53 -07:00
868ea9362f make open in same tab as default option for url 2017-09-25 16:04:23 -07:00
d69106ff2c Merge remote-tracking branch 'origin/master' into summary-widgets
# Conflicts resolved:
#	platform/commonUI/general/res/sass/user-environ/_frame.scss
2017-09-25 12:33:42 -07:00
1658b17c56 add hyperlink functionality 2017-09-25 11:35:12 -07:00
39cf0528ca merge changes from charles 2017-09-25 10:04:44 -07:00
3d12f7312b checkstyle error fix 2017-09-25 10:04:25 -07:00
22481fdc31 [Frontend] Set default widget colors and icon
Fixes #1668
2017-09-22 12:05:32 -07:00
4a9d27dc79 [Frontend] Hover behaviors refined
Fixes #1668
2017-09-22 11:41:00 -07:00
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
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
a540a3573f [Front-end] WIP Styling for widget in layout
Fixes #1668
2017-09-21 15:30:12 -07:00
4e7fe9082c Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 15:26:47 -07:00
568141bf81 [Front-end] WIP Styling for widget in layout
Fixes #1668
2017-09-21 15:26:28 -07:00
ac3ea43fe5 hide equal to label until select field is available 2017-09-21 15:22:55 -07:00
e922e8d504 [Front-end] Add summary widgets to hide-buttons-in-Layout list
Fixes #1668
2017-09-21 15:11:15 -07:00
650a877d2a [Front-end] Inject icon directly into widget Label
Fixes #1668
Remove .widget-icon element;
2017-09-21 15:04:35 -07:00
1202109c59 Merge branch 'summary-widgets-styling-2' of https://github.com/nasa/openmct into summary-widgets 2017-09-21 14:53:27 -07:00
429d7bbd57 add hide/unhide to select.js 2017-09-21 14:53:06 -07:00
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
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
c5bd3da44a Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 11:39:04 -07:00
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
e244a3e431 [Front-end] New null value selection strings
Fixes #1668
2017-09-21 11:23:26 -07:00
c5d9fb6fd9 [Front-end] Hide/show "no-data" message element
Fixes #1688
2017-09-21 11:11:12 -07:00
4c276ab422 [Front-end] Normalize font sizing in overlay
Fixes #1688
2017-09-21 10:26:47 -07:00
bf321abae4 Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling-2 2017-09-21 10:09:57 -07:00
7336968ef9 [Frontend] Add message markup and styling
Fixes #1688
Add to Summary Widgets UI;
2017-09-20 18:03:03 -07:00
d60956948b [Frontend] Sanding to avoid CSS collision
Fixes #1688
2017-09-20 18:02:26 -07:00
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
2632b8891a Merge latest styling changes from Charles 2017-09-20 14:56:05 -07:00
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
be0291cf70 [Frontend] Removed t-condition from testDataItem
Fixes #1668
Fixes #1644
2017-09-19 17:51:16 -07:00
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
ebeed2f236 [Frontend] Significant styling for test data area
Fixes #1668
Fixes #1644
2017-09-19 17:37:15 -07:00
337c26c019 fix failing tests 2017-09-18 13:12:23 -07:00
730f363f94 Merge branch 'summary-widgets-styling-2' of https://github.com/nasa/openmct into summary-widgets 2017-09-18 09:44:15 -07:00
ae30e6110b merge from styling 2017-09-18 09:44:07 -07:00
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
5465ca92f9 [Frontend] Flex layout now working!
Fixes #1668
Fixes #1644
2017-09-15 17:33:28 -07:00
e55ea41b0a [Frontend] Align selects
Fixes #1668
Fixes #1644
2017-09-15 17:02:20 -07:00
8cfb3cc689 [Frontend] Merge latest master
Fixes #1668
Fixes #1644
2017-09-15 17:01:32 -07:00
2d728a1362 [Frontend] WIP Styling continued
Fixes #1668
Markup tweaks
2017-09-15 16:44:11 -07:00
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
de783d4286 fix circle-ci build errors 2017-09-14 14:11:02 -07:00
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
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
38f0f072bb [Merge] Ellipsize added to l-summary-widget
Fixes #1669
2017-08-25 14:19:43 -07:00
e5e969665f [Merge] Merge latest master
Fixes #1669
Merge + resolve conflicts;
2017-08-25 14:11:34 -07:00
ffbb662c99 [Summary Widgets] Unit tests 2017-08-25 14:09:22 -07:00
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
c238def902 [Summary Widgets] Unit tests 2017-08-25 11:57:29 -07:00
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
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
41ce3c04f7 [Summary Widgets] Unit Tests 2017-08-24 16:52:44 -07:00
fcf77f359f [Summary Widgets] Unit tests 2017-08-24 12:06:31 -07:00
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
216489d67f [Summary Widgets] Tests
Add unit tests
2017-08-23 17:01:14 -07:00
418a393b26 [Sumamry Widgets] Destroy
Implement destroy
2017-08-23 13:09:36 -07:00
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
ff3f2dccba [Summary Widget] Tests
Add and update unit tests

Fix style in markup

Begin implementing destroy
2017-08-22 16:51:00 -07:00
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
05b352cc36 [Summary Widgets] Documentation
Add JSDoc style comments
2017-08-21 17:05:41 -07:00
9735548999 [Summary Widgets] Add documentation 2017-08-18 16:22:23 -07:00
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
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
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
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
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
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
d0745b300b Allow views to be editable 2017-08-16 11:20:04 -07:00
2a4e0a3081 [Frontend] Mods to custom selects
Fixes #1669
line-height, context arrow positioning and color
2017-08-16 10:50:08 -07:00
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
7ef84cb50d [Front-end] Fix palette menu when no icon is selected
Fixes #1669
2017-08-15 15:51:32 -07:00
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
568473b82f [Summary Widgets] Rule Reorders
Cleanup Widget DnD code and package it as a module
2017-08-14 16:24:38 -07:00
c61b074755 [Summary Widgets] Rule Reorders
Re-implement drag and drop with a 'sortable list' style interface
2017-08-14 14:21:37 -07:00
8ed66ab4ab [Summary Widgets] Rule Reorders
Implement drag and drop rule reorders using the native HTML5 API
2017-08-11 16:57:40 -07:00
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
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
0c0ca6e6af [Frontend] Colors for palette defined
Fixes #1669
2017-08-09 16:00:33 -07:00
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
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
8a8e3cc055 [Front-end] WIP colors for Summary Widget palettes
Fixes #1669
2017-08-08 16:11:52 -07:00
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
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
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
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
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
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
7b62cf130c [Summary Widget] Add unit tests
Add tests for input classes

Fix code style errors
2017-08-07 17:07:21 -07:00
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
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
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
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
405bb55881 [Refactor] Cleanup old files 2017-08-01 13:51:20 -07:00
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
98a9d71a2e [Summary Widgets] Implementation for conditions
Support configuring and persisting multiple conditions per rule
2017-07-26 09:33:27 -07:00
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
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
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
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
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
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
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
500733afb2 [Frontend] Add new icon-grippy
Fixes #1654
2017-07-21 10:25:18 -07:00
2aa04b0a56 Merge remote-tracking branch 'origin/summary-widgets' into summary-widgets-styling 2017-07-21 10:04:23 -07:00
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
8aeb365f5f [Frontend] WIP summary widget styling
Fixes #1654
Add condition duplicate and delete buttons
2017-07-21 09:56:57 -07:00
827a28313d [Frontend] WIP summary widget styling
Fixes #1654
Fixes and tweaks post-merge
2017-07-21 09:48:54 -07:00
c83de8aad2 [Summary Widgets] Minor UI implementation 2017-07-21 09:24:27 -07:00
b55f43b8df [Frontend] WIP summary widget styling
Fixes #1654
Merge latest from summary-widgets branch
2017-07-21 09:15:19 -07:00
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
a103b4dbff [Frontend] WIP summary widget styling
Fixes #1654
Significant mods to markup and SCSS;
2017-07-20 17:56:57 -07:00
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
597327f138 [Frontend] WIP summary widget styling
Fixes #1654
New SCSS for widgets; markup mods in progress
2017-07-20 14:09:17 -07:00
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
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
c73f7259c2 [Frontend] WIP summary widget styling
Fixes #1654
Markup cleanups
2017-07-19 15:38:44 -07:00
4c9235ba10 [Frontend] Apply new summary-widget glyph
Fixes #1654
2017-07-19 15:01:04 -07:00
55e2a77df8 [Frontend] Add new summary-widget glyph
Fixes #1654
Updated font files and icomoon json file;
2017-07-19 15:00:46 -07:00
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
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
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
107 changed files with 7969 additions and 342 deletions

15
API.md
View File

@ -879,6 +879,21 @@ openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
* `openmct.plugins.Espresso` and `openmct.plugins.Snow` are two different
themes (dark and light) available for Open MCT. Note that at least one
of these themes must be installed for Open MCT to appear correctly.
* `openmct.plugins.URLIndicatorPlugin` adds an indicator which shows the
availability of a URL with the following options:
- `url` : URL to indicate the status of
- `cssClass`: Icon to show in the status bar, defaults to `icon-database`, [list of all icons](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home?view=items)
- `interval`: Interval between checking the connection, defaults to `10000`
- `label` Name showing up as text in the status bar, defaults to url
```javascript
openmct.install(openmct.plugins.URLIndicatorPlugin({
url: 'http://google.com',
cssClass: 'check',
interval: 10000,
label: 'Google'
})
);
```
* `openmct.plugins.LocalStorage` provides persistence of user-created
objects in browser-local storage. This is particularly useful in
development environments.

View File

@ -17,7 +17,7 @@
"screenfull": "^3.0.0",
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"file-saver": "^1.3.3",
"zepto": "^1.1.6",
"eventemitter3": "^1.2.0",
"lodash": "3.10.1",

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]) {

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

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

@ -33,7 +33,7 @@ requirejs.config({
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"saveAs": "bower_components/file-saver/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",

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

@ -101,10 +101,15 @@ define(
*/
EditorCapability.prototype.finish = function () {
var domainObject = this.domainObject;
return this.transactionService.cancel().then(function () {
domainObject.getCapability("status").set("editing", false);
return domainObject;
});
if (this.transactionService.isActive()) {
return this.transactionService.cancel().then(function () {
domainObject.getCapability("status").set("editing", false);
return domainObject;
});
} else {
return Promise.resolve(domainObject);
}
};
/**

View File

@ -62,6 +62,7 @@ define(
);
mockTransactionService.commit.andReturn(fastPromise());
mockTransactionService.cancel.andReturn(fastPromise());
mockTransactionService.isActive = jasmine.createSpy('isActive');
mockStatusCapability = jasmine.createSpyObj(
"statusCapability",
@ -141,6 +142,7 @@ define(
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.andReturn(true);
capability.edit();
capability.finish();
});
@ -152,6 +154,23 @@ define(
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.andReturn(false);
capability.edit();
});
it("does not cancel transaction when transaction is not active", function () {
capability.finish();
expect(mockTransactionService.cancel).not.toHaveBeenCalled();
});
it("returns a promise", function () {
expect(capability.finish() instanceof Promise).toBe(true);
});
});
describe("dirty", function () {
var model = {};

View File

@ -137,6 +137,11 @@
min-height: 0;
&.holder:not(:last-child) { margin-bottom: $interiorMarginLg; }
}
&.l-flex-accordion .flex-accordion-holder {
display: flex;
flex-direction: column;
//overflow: hidden !important;
}
.flex-container { @include flex-direction(column); }
}

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

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

@ -50,7 +50,6 @@
content:'';
font-family: symbolsfont;
font-size: 0.8em;
display: inline;
margin-right: $interiorMarginSm;
}
}

View File

@ -0,0 +1,306 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/************************************************************* WIDGET OBJECT */
.l-summary-widget {
// Widget layout classes here
@include ellipsize();
display: inline-block;
text-align: center;
.widget-label:before {
// Widget icon
font-size: 0.9em;
margin-right: $interiorMarginSm;
}
}
.s-summary-widget {
// Widget style classes here
@include boxShdw($shdwBtns);
border-radius: $basicCr;
border-style: solid;
border-width: 1px;
box-sizing: border-box;
cursor: default;
font-size: 0.8rem;
padding: $interiorMarginLg $interiorMarginLg * 2;
&[href] {
cursor: pointer;
}
}
.widget-edit-holder {
// Hide edit area when in browse mode
display: none;
}
.widget-rule-header {
@extend .l-flex-row;
@include align-items(center);
margin-bottom: $interiorMargin;
> .flex-elem {
&:not(:first-child) {
margin-left: $interiorMargin;
}
}
}
.widget-rules-wrapper,
.widget-rule-content,
.w-widget-test-data-content {
@include trans-prop-nice($props: (height, min-height, opacity), $dur: 250ms);
min-height: 0;
height: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
}
.widget-rules-wrapper {
flex: 1 1 auto !important;
}
.widget-rule-content.expanded {
overflow: visible !important;
min-height: 50px;
height: auto;
opacity: 1;
pointer-events: inherit;
}
.w-widget-test-data-content {
.l-enable {
padding: $interiorMargin 0;
}
.w-widget-test-data-items {
max-height: 20vh;
overflow-y: scroll !important;
padding-right: $interiorMargin;
}
}
.l-widget-thumb-wrapper,
.l-compact-form label {
$ruleLabelW: 40%;
$ruleLabelMaxW: 150px;
@include display(flex);
max-width: $ruleLabelMaxW;
width: $ruleLabelW;
}
.t-message-widget-no-data {
display: none;
}
/********************************************************** EDITING A WIDGET */
.s-status-editing > mct-view > .w-summary-widget {
// Classes for editor layout while editing a widget
// This selector is ugly and brittle, but needed to prevent interface from showing when widget is in a layout
// being edited.
@include absPosDefault();
@extend .l-flex-col;
> .l-summary-widget {
// Main view of the summary widget
// Give some airspace and center the widget in the area
margin: 30px auto;
}
.widget-edit-holder {
display: flex; // Overrides `display: none` during Browse mode
.flex-accordion-holder {
// Needed because otherwise accordion elements "creep" when contents expand and contract
display: block !important;
}
&.expanded-widget-test-data {
.w-widget-test-data-content {
min-height: 50px;
height: auto;
opacity: 1;
pointer-events: inherit;
}
&:not(.expanded-widget-rules) {
// Test data is expanded and rules are collapsed
// Make text data take up all the vertical space
.flex-accordion-holder { display: flex; }
.widget-test-data {
flex-grow: 999999;
}
.w-widget-test-data-items {
max-height: inherit;
}
}
}
&.expanded-widget-rules {
.widget-rules-wrapper {
min-height: 50px;
height: auto;
opacity: 1;
pointer-events: inherit;
}
}
}
&.s-status-no-data {
.widget-edit-holder {
opacity: 0.3;
pointer-events: none;
}
.t-message-widget-no-data {
display: flex;
}
}
.l-compact-form {
// Overrides on .l-compact-form
ul {
&:last-child { margin: 0; }
li {
@include align-items(flex-start);
@include flex-wrap(nowrap);
line-height: 230%; // Provide enough space when controls wrap
padding: 2px 0;
&:not(.widget-rule-header) {
&:not(.connects-to-previous) {
border-top: 1px solid $colorFormLines;
}
}
&.connects-to-previous {
padding: $interiorMargin 0;
}
> label {
display: block; // Needed to align text to right
text-align: right;
}
}
}
&.s-widget-test-data-item {
// Single line of ul li label span, etc.
ul {
li {
border: none !important;
> label {
display: inline-block;
width: auto;
text-align: left;
}
}
}
}
}
}
.widget-edit-holder {
font-size: 0.8rem;
}
.widget-rules-wrapper {
// Wrapper area that holds n rules
box-sizing: border-box;
overflow-y: scroll;
padding-right: $interiorMargin;
}
.l-widget-rule,
.l-widget-test-data-item {
box-sizing: border-box;
margin-bottom: $interiorMarginSm;
padding: $interiorMargin $interiorMarginLg;
}
.l-widget-thumb-wrapper {
@extend .l-flex-row;
@include align-items(center);
> span { display: block; }
.grippy-holder,
.view-control {
margin-right: $interiorMargin;
width: 1em;
height: 1em;
}
.widget-thumb {
@include flex(1 1 auto);
width: 100%;
}
}
.rule-title {
@include flex(0 1 auto);
color: pullForward($colorBodyFg, 50%);
}
.rule-description {
@include flex(1 1 auto);
@include ellipsize();
color: pushBack($colorBodyFg, 20%);
}
.s-widget-rule,
.s-widget-test-data-item {
background-color: rgba($colorBodyFg, 0.1);
border-radius: $basicCr;
}
.widget-thumb {
@include ellipsize();
@extend .s-summary-widget;
@extend .l-summary-widget;
padding: $interiorMarginSm $interiorMargin;
}
// Hide and show elements in the rule-header on hover
.l-widget-rule,
.l-widget-test-data-item {
.grippy,
.l-rule-action-buttons-wrapper,
.l-condition-action-buttons-wrapper,
.l-widget-test-data-item-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 500ms);
opacity: 0;
}
&:hover {
.grippy,
.l-rule-action-buttons-wrapper,
.l-widget-test-data-item-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 0);
opacity: 1;
}
}
.l-rule-action-buttons-wrapper {
.t-delete {
margin-left: 10px;
}
}
.t-condition {
&:hover {
.l-condition-action-buttons-wrapper {
@include trans-prop-nice($props: opacity, $dur: 0);
opacity: 1;
}
}
}
}

View File

@ -261,7 +261,7 @@ input[type="number"] {
input[type="text"].lg { width: 100% !important; }
.l-input-med input[type="text"],
input[type="text"].med { width: 200px !important; }
input[type="text"].sm { width: 50px !important; }
input[type="text"].sm, input[type="number"].sm { width: 50px !important; }
.l-numeric input[type="text"],
input[type="text"].numeric { text-align: right; }
@ -317,14 +317,10 @@ input[type="text"].s-input-inline,
.select {
@include btnSubtle($bg: $colorSelectBg);
@extend .icon-arrow-down; // Context arrow
@if $shdwBtns != none {
margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers
}
display: inline-block;
padding: 0 $interiorMargin;
overflow: hidden;
position: relative;
line-height: $formInputH;
select {
@include appearance(none);
box-sizing: border-box;
@ -340,11 +336,13 @@ input[type="text"].s-input-inline,
}
}
&:before {
pointer-events: none;
@include transform(translateY(-50%));
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
display: block;
pointer-events: none;
position: absolute;
right: $interiorMargin; top: 0;
right: $interiorMargin;
top: 50%;
}
}
@ -396,8 +394,7 @@ input[type="text"].s-input-inline,
.l-elem-wrapper {
mct-representation {
// Holds the context-available item
// Must have min-width to make flex work properly
// in Safari
// Must have min-width to make flex work properly in Safari
min-width: 0.7em;
}
}
@ -563,7 +560,6 @@ input[type="text"].s-input-inline,
height: $h;
margin-top: 1 + floor($h/2) * -1;
@include btnSubtle(pullForward($colorBtnBg, 10%));
//border-radius: 50% !important;
}
@mixin sliderKnobRound() {
@ -578,7 +574,6 @@ input[type="text"].s-input-inline,
input[type="range"] {
// HTML5 range inputs
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
background: transparent; /* Otherwise white in Chrome */
&:focus {
@ -736,6 +731,30 @@ textarea {
}
}
.view-switcher,
.t-btn-view-large {
@include trans-prop-nice-fade($controlFadeMs);
}
.view-control {
@extend .icon-arrow-right;
cursor: pointer;
font-size: 0.75em;
&:before {
position: absolute;
@include trans-prop-nice(transform, 100ms);
@include transform-origin(center);
}
&.expanded:before {
@include transform(rotate(90deg));
}
}
.grippy {
@extend .icon-grippy;
cursor: move;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {

View File

@ -29,23 +29,27 @@
}
.icon {
font-size: 16px; //120%;
font-size: 16px;
}
.title-label {
margin-left: $interiorMarginSm;
}
.icon-swatch,
.color-swatch {
// Used in color menu buttons in toolbar
$d: 10px;
display: inline-block;
border: 1px solid rgba($colorBtnFg, 0.2);
height: $d;
width: $d;
height: $d; width: $d;
line-height: $d;
vertical-align: middle;
margin-left: $interiorMarginSm;
margin-top: -2px;
&:not(.no-selection) {
border-color: transparent;
}
}
&:after {

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%;
@ -120,7 +120,11 @@
}
.status-indicator {
background: none !important;
margin-right: $interiorMarginSm;
&[class*='s-status']:before {
font-size: 1em;
}
}
.count {
@ -136,7 +140,7 @@
}
}
/* Styles for messages and message banners */
/******************************************************************* MESSAGE BANNERS */
.message {
&.block {
border-radius: $basicCr;
@ -192,7 +196,6 @@
padding: 0 $interiorMargin;
}
.close {
//@include test(red, 0.7);
cursor: pointer;
font-size: 7px;
width: 8px;
@ -236,132 +239,147 @@
}
}
@mixin messageBlock($iconW: 32px) {
.type-icon.message-type {
/******************************************************************* MESSAGES */
/* Contexts:
In .t-message-list
In .overlay as a singleton
Inline in the view area
*/
// Archetypal message
.l-message {
$iconW: 32px;
@include display(flex);
@include flex-direction(row);
@include align-items(stretch);
padding: $interiorMarginLg;
&:before {
// Icon
@include flex(0 1 auto);
@include txtShdw($shdwStatusIc);
@extend .icon-bell;
color: $colorStatusDefault;
font-size: $iconW;
padding: 1px;
width: $iconW + 2;
margin-right: $interiorMarginLg;
}
.message-severity-info .type-icon.message-type {
&.message-severity-info:before {
@extend .icon-info;
color: $colorInfo;
}
.message-severity-alert .type-icon.message-type {
@extend .icon-bell;
&.message-severity-alert:before {
color: $colorWarningLo;
}
.message-severity-error .type-icon.message-type {
&.message-severity-error:before {
@extend .icon-alert-rect;
color: $colorWarningHi;
}
}
/* Paths:
t-dialog | t-dialog-sm > t-message-single | t-message-list > overlay > holder > contents > l-message >
message-type > (icon)
message-contents >
top-bar >
title
hint
editor >
(if displaying list of messages)
ul > li > l-message >
... same as above
bottom-bar
*/
.l-message {
.w-message-contents {
@include flex(1 1 auto);
@include display(flex);
@include flex-direction(row);
@include align-items(stretch);
.type-icon.message-type {
@include flex(0 1 auto);
position: relative;
}
.message-contents {
@include flex(1 1 auto);
margin-left: $overlayMargin;
position: relative;
@include flex-direction(column);
.top-bar,
> div,
> span {
//@include test(red);
margin-bottom: $interiorMargin;
}
.message-body {
@include flex(1 1 100%);
}
}
// Singleton in an overlay dialog
.t-message-single .l-message,
.t-message-single.l-message {
$iconW: 80px;
@include absPosDefault();
padding: 0;
&:before {
font-size: $iconW;
width: $iconW + 2;
}
.title {
font-size: 1.2em;
}
}
// Singleton inline in a view
.t-message-inline .l-message,
.t-message-inline.l-message {
border-radius: $controlCr;
&.message-severity-info { background-color: rgba($colorInfo, 0.3); }
&.message-severity-alert { background-color: rgba($colorWarningLo, 0.3); }
&.message-severity-error { background-color: rgba($colorWarningHi, 0.3); }
.w-message-contents.l-message-body-only {
.message-body {
margin-bottom: $interiorMarginLg * 2;
margin-top: $interiorMargin;
}
}
}
// In a list
.t-message-list {
@include absPosDefault();
@include display(flex);
@include flex-direction(column);
// Message as singleton
.t-message-single {
@include messageBlock(80px);
}
body.desktop .t-message-single {
.l-message,
.bottom-bar {
@include absPosDefault();
> div,
> span {
margin-bottom: $interiorMargin;
}
.bottom-bar {
top: auto;
height: $ovrFooterH;
.w-messages {
@include flex(1 1 100%);
overflow-y: auto;
padding-right: $interiorMargin;
}
// Each message
.l-message {
border-radius: $controlCr;
background: rgba($colorOvrFg, 0.1);
margin-bottom: $interiorMargin;
.hint,
.bottom-bar {
text-align: left;
}
}
}
@include phonePortrait {
.t-message-single {
.l-message {
@include flex-direction(column);
.message-contents { margin-left: 0; }
}
.type-icon.message-type {
.t-message-single .l-message,
.t-message-single.l-message {
@include flex-direction(column);
&:before {
margin-right: 0;
margin-bottom: $interiorMarginLg;
width: 100%;
text-align: center;
width: 100%;
}
.bottom-bar {
text-align: center !important;
}
}
}
// Messages in list
.t-message-list {
@include messageBlock(32px);
.message-contents {
.l-message {
border-radius: $controlCr;
background: rgba($colorOvrFg, 0.1);
margin-bottom: $interiorMargin;
padding: $interiorMarginLg;
.message-contents,
.bottom-bar {
position: relative;
}
.message-contents {
font-size: 0.9em;
margin-left: $interiorMarginLg;
.message-action { color: pushBack($colorOvrFg, 20%); }
.bottom-bar { text-align: left; }
}
.top-bar,
.message-body {
margin-bottom: $interiorMarginLg;
text-align: center;
.s-button {
display: block;
width: 100%;
}
}
}
}
body.desktop .t-message-list {
.message-contents .l-message { margin-right: $interiorMarginLg; }
.w-message-contents { padding-right: $interiorMargin; }
}
// Alert elements in views

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

@ -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;
}

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.
@ -125,14 +126,21 @@
pointer-events: none !important;
}
/********************************************************** OBJECT TYPES */
.t-object-type-hyperlink {
/********************************************************** OBJECT TYPES */
.t-object-type-hyperlink,
.t-object-type-summary-widget {
.object-holder {
overflow: hidden;
}
.w-summary-widget,
.l-summary-widget,
.l-hyperlink.s-button {
// When a hyperlink is a button in a frame, make it expand to fill out to the object-holder
// Some object types expand to the full size of the object-holder.
@extend .abs;
}
.l-summary-widget,
.l-hyperlink.s-button {
.label {
@include ellipsize();
@include transform(translateY(-50%));

View File

@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
// Palettes
$colorPaletteFg: pullForward($colorMenuBg, 30%);
$colorPaletteSelected: #fff;
$shdwPaletteFg: black 0 0 2px;
$shdwPaletteSelected: inset 0 0 0 1px #000;
// About Screen
$colorAboutLink: #84b3ff;

View File

@ -243,6 +243,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
// Palettes
$colorPaletteFg: pullForward($colorMenuBg, 30%);
$colorPaletteSelected: #333;
$shdwPaletteFg: none;
$shdwPaletteSelected: inset 0 0 0 1px #fff;
// About Screen
$colorAboutLink: #84b3ff;

View File

@ -23,10 +23,13 @@
define([
"moment-timezone",
"./src/indicators/ClockIndicator",
"./src/indicators/FollowIndicator",
"./src/services/TickerService",
"./src/services/TimerService",
"./src/controllers/ClockController",
"./src/controllers/TimerController",
"./src/controllers/RefreshingController",
"./src/actions/FollowTimerAction",
"./src/actions/StartTimerAction",
"./src/actions/RestartTimerAction",
"./src/actions/StopTimerAction",
@ -37,10 +40,13 @@ define([
], function (
MomentTimezone,
ClockIndicator,
FollowIndicator,
TickerService,
TimerService,
ClockController,
TimerController,
RefreshingController,
FollowTimerAction,
StartTimerAction,
RestartTimerAction,
StopTimerAction,
@ -80,6 +86,11 @@ define([
"CLOCK_INDICATOR_FORMAT"
],
"priority": "preferred"
},
{
"implementation": FollowIndicator,
"depends": ["timerService"],
"priority": "fallback"
}
],
"services": [
@ -90,6 +101,11 @@ define([
"$timeout",
"now"
]
},
{
"key": "timerService",
"implementation": TimerService,
"depends": ["openmct"]
}
],
"controllers": [
@ -134,6 +150,15 @@ define([
}
],
"actions": [
{
"key": "timer.follow",
"implementation": FollowTimerAction,
"depends": ["timerService"],
"category": "contextual",
"name": "Follow Timer",
"cssClass": "icon-clock",
"priority": "optional"
},
{
"key": "timer.start",
"implementation": StartTimerAction,

View File

@ -0,0 +1,56 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* Designates a specific timer for following. Timelines, for example,
* use the actively followed timer to display a time-of-interest line
* and interpret time conductor bounds in the Timeline's relative
* time frame.
*
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {ActionContext} context the context for this action
*/
function FollowTimerAction(timerService, context) {
var domainObject =
context.domainObject &&
context.domainObject.useCapability('adapter');
this.perform =
timerService.setTimer.bind(timerService, domainObject);
}
FollowTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel()) ||
{};
return model.type === 'timer';
};
return FollowTimerAction;
}
);

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['moment'],
function (moment) {
var NO_TIMER = "No timer being followed";
/**
* Indicator that displays the active timer, as well as its
* current state.
* @implements {Indicator}
* @memberof platform/features/clock
*/
function FollowIndicator(timerService) {
this.timerService = timerService;
}
FollowIndicator.prototype.getGlyphClass = function () {
return "";
};
FollowIndicator.prototype.getCssClass = function () {
return (this.timerService.getTimer()) ? "icon-timer s-status-ok" : "icon-timer";
};
FollowIndicator.prototype.getText = function () {
var timer = this.timerService.getTimer();
return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER;
};
FollowIndicator.prototype.getDescription = function () {
return "";
};
return FollowIndicator;
}
);

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['EventEmitter'], function (EventEmitter) {
/**
* Tracks the currently-followed Timer object. Used by
* timelines et al to synchronize to a particular timer.
*
* The TimerService emits `change` events when the active timer
* is changed.
*/
function TimerService(openmct) {
EventEmitter.apply(this);
this.time = openmct.time;
this.objects = openmct.objects;
}
TimerService.prototype = Object.create(EventEmitter.prototype);
/**
* Set (or clear, if `timer` is undefined) the currently active timer.
* @param {DomainObject} timer the new active timer
* @emits change
*/
TimerService.prototype.setTimer = function (timer) {
this.timer = timer;
this.emit('change');
if (this.stopObserving) {
this.stopObserving();
delete this.stopObserving;
}
if (timer) {
this.stopObserving =
this.objects.observe(timer, '*', this.setTimer.bind(this));
}
};
/**
* Get the currently active timer.
* @return {DomainObject} the active timer
* @emits change
*/
TimerService.prototype.getTimer = function () {
return this.timer;
};
/**
* Check if there is a currently active timer.
* @return {boolean} true if there is a timer
*/
TimerService.prototype.hasTimer = function () {
return !!this.timer;
};
/**
* Convert the provided timestamp to milliseconds relative to
* the active timer.
* @return {number} milliseconds since timer start
*/
TimerService.prototype.convert = function (timestamp) {
var clock = this.time.clock();
var canConvert = this.hasTimer() &&
!!clock &&
this.timer.timerState !== 'stopped';
if (!canConvert) {
return undefined;
}
var now = clock.currentValue();
var delta = this.timer.timerState === 'paused' ?
now - this.timer.pausedTime : 0;
var epoch = this.timer.timestamp;
return timestamp - epoch - delta;
};
/**
* Get the value of the active clock, adjusted to be relative to the active
* timer. If there is no clock or no active timer, this will return
* `undefined`.
* @return {number} milliseconds since the start of the active timer
*/
TimerService.prototype.now = function () {
var clock = this.time.clock();
return clock && this.convert(clock.currentValue());
};
return TimerService;
});

View File

@ -0,0 +1,87 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"../../src/actions/FollowTimerAction"
], function (FollowTimerAction) {
var TIMER_SERVICE_METHODS =
['setTimer', 'getTimer', 'clearTimer', 'on', 'off'];
describe("The Follow Timer action", function () {
var testContext;
var testModel;
var testAdaptedObject;
beforeEach(function () {
testModel = {};
testContext = { domainObject: jasmine.createSpyObj('domainObject', [
'getModel',
'useCapability'
]) };
testAdaptedObject = { foo: 'bar' };
testContext.domainObject.getModel.andReturn(testModel);
testContext.domainObject.useCapability.andCallFake(function (c) {
return c === 'adapter' && testAdaptedObject;
});
});
it("is applicable to timers", function () {
testModel.type = "timer";
expect(FollowTimerAction.appliesTo(testContext)).toBe(true);
});
it("is inapplicable to non-timers", function () {
testModel.type = "folder";
expect(FollowTimerAction.appliesTo(testContext)).toBe(false);
});
describe("when instantiated", function () {
var mockTimerService;
var action;
beforeEach(function () {
mockTimerService = jasmine.createSpyObj(
'timerService',
TIMER_SERVICE_METHODS
);
action = new FollowTimerAction(mockTimerService, testContext);
});
it("does not interact with the timer service", function () {
TIMER_SERVICE_METHODS.forEach(function (method) {
expect(mockTimerService[method]).not.toHaveBeenCalled();
});
});
describe("and performed", function () {
beforeEach(function () {
action.perform();
});
it("sets the active timer", function () {
expect(mockTimerService.setTimer)
.toHaveBeenCalledWith(testAdaptedObject);
});
});
});
});
});

View File

@ -0,0 +1,61 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) {
var TIMER_SERVICE_METHODS =
['setTimer', 'getTimer', 'clearTimer', 'on', 'off'];
describe("The timer-following indicator", function () {
var mockTimerService;
var indicator;
beforeEach(function () {
mockTimerService =
jasmine.createSpyObj('timerService', TIMER_SERVICE_METHODS);
indicator = new FollowIndicator(mockTimerService);
});
it("implements the Indicator interface", function () {
expect(indicator.getGlyphClass()).toEqual(jasmine.any(String));
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
describe("when a timer is set", function () {
var testModel;
var mockDomainObject;
beforeEach(function () {
testModel = { name: "some timer!" };
mockDomainObject = jasmine.createSpyObj('timer', ['getModel']);
mockDomainObject.getModel.andReturn(testModel);
mockTimerService.getTimer.andReturn(mockDomainObject);
});
it("displays the timer's name", function () {
expect(indicator.getText().indexOf(testModel.name))
.not.toEqual(-1);
});
});
});
});

View File

@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../../src/services/TimerService'
], function (TimerService) {
describe("TimerService", function () {
var callback;
var mockmct;
var timerService;
beforeEach(function () {
callback = jasmine.createSpy('callback');
mockmct = {
time: { clock: jasmine.createSpy('clock') },
objects: { observe: jasmine.createSpy('observe') }
};
timerService = new TimerService(mockmct);
timerService.on('change', callback);
});
it("initially emits no change events", function () {
expect(callback).not.toHaveBeenCalled();
});
it("reports no current timer", function () {
expect(timerService.getTimer()).toBeUndefined();
});
describe("setTimer", function () {
var testTimer;
beforeEach(function () {
testTimer = { name: "I am some timer; you are nobody." };
timerService.setTimer(testTimer);
});
it("emits a change event", function () {
expect(callback).toHaveBeenCalled();
});
it("reports the current timer", function () {
expect(timerService.getTimer()).toBe(testTimer);
});
it("observes changes to an object", function () {
var newTimer = { name: "I am another timer." };
expect(mockmct.objects.observe).toHaveBeenCalledWith(
testTimer,
'*',
jasmine.any(Function)
);
mockmct.objects.observe.mostRecentCall.args[2](newTimer);
expect(timerService.getTimer()).toBe(newTimer);
});
});
});
});

View File

@ -255,6 +255,8 @@ define(
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
} else {
this.updateValues(this.$scope.imageHistory[this.$scope.imageHistory.length - 1]);
}
this.autoScroll = true;
}

View File

@ -183,6 +183,17 @@ define(
expect(controller.getImageUrl()).toEqual(newUrl);
});
it("forwards large image view to latest image in history on un-pause", function () {
$scope.imageHistory = [
{ utc: 1434600258122, url: 'some/url1', selected: false},
{ utc: 1434600258123, url: 'some/url2', selected: false}
];
controller.paused(true);
controller.paused(false);
expect(controller.getImageUrl()).toEqual(controller.getImageUrl($scope.imageHistory[1]));
});
it("subscribes to telemetry", function () {
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
newDomainObject,
@ -227,7 +238,7 @@ define(
expect(controller.updateHistory(mockDatum)).toBe(false);
});
describe("user clicks on imagery thumbnail", function () {
describe("when user clicks on imagery thumbnail", function () {
var mockDatum = { utc: 1434600258123, url: 'some/url', selected: false};
it("pauses and adds selected class to imagery thumbnail", function () {
@ -248,6 +259,7 @@ define(
expect(controller.getTime()).toEqual(controller.timeFormat.format(mockDatum.utc));
});
});
});
it("initially shows an empty string for date/time", function () {

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

View File

@ -360,22 +360,47 @@ define(
*/
FixedController.prototype.updateView = function (telemetryObject, datum) {
var metadata = this.openmct.telemetry.getMetadata(telemetryObject);
var rangeMetadata = metadata.valuesForHints(['range'])[0];
var rangeKey = rangeMetadata.source || rangeMetadata.key;
var valueMetadata = metadata.value(rangeKey);
var telemetryKeyToDisplay = this.chooseTelemetryKeyToDisplay(metadata);
var formattedTelemetryValue = this.getFormattedTelemetryValueForKey(telemetryKeyToDisplay, datum, metadata);
var limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
var formatter = this.openmct.telemetry.getValueFormatter(valueMetadata);
var value = datum[valueMetadata.key];
var alarm = limitEvaluator && limitEvaluator.evaluate(datum, rangeKey);
var alarm = limitEvaluator && limitEvaluator.evaluate(datum, telemetryKeyToDisplay);
this.setDisplayedValue(
telemetryObject,
formatter.format(value),
formattedTelemetryValue,
alarm && alarm.cssClass
);
this.digest();
};
/**
* @private
*/
FixedController.prototype.getFormattedTelemetryValueForKey = function (telemetryKeyToDisplay, datum, metadata) {
var valueMetadata = metadata.value(telemetryKeyToDisplay);
var formatter = this.openmct.telemetry.getValueFormatter(valueMetadata);
return formatter.format(datum[valueMetadata.key]);
};
/**
* @private
*/
FixedController.prototype.chooseTelemetryKeyToDisplay = function (metadata) {
// If there is a range value, show that preferentially
var telemetryKeyToDisplay = metadata.valuesForHints(['range'])[0];
// If no range is defined, default to the highest priority non time-domain data.
if (telemetryKeyToDisplay === undefined) {
var valuesOrderedByPriority = metadata.values();
telemetryKeyToDisplay = valuesOrderedByPriority.filter(function (valueMetadata) {
return !(valueMetadata.hints.domain);
})[0];
}
return telemetryKeyToDisplay.source;
};
/**
* Request the last historical data point for the given domain objects
* @param {object[]} objects
@ -388,7 +413,9 @@ define(
objects.forEach(function (object) {
self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1})
.then(function (data) {
self.updateView(object, data[data.length - 1]);
if (data.length > 0) {
self.updateView(object, data[data.length - 1]);
}
});
});
return objects;

View File

@ -127,9 +127,7 @@ define(
if (self.droppedIdToSelectAfterRefresh) {
self.select(null, self.droppedIdToSelectAfterRefresh);
delete self.droppedIdToSelectAfterRefresh;
}
if (composition.indexOf(self.selectedId) === -1) {
} else if (composition.indexOf(self.selectedId) === -1) {
self.clearSelection();
}
}

View File

@ -178,7 +178,6 @@ define(
Promise.resolve(mockChildren)
);
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
mockScope.selection = jasmine.createSpyObj(
@ -194,7 +193,8 @@ define(
mockMetadata = jasmine.createSpyObj('mockMetadata', [
'valuesForHints',
'value'
'value',
'values'
]);
mockMetadata.value.andReturn({
key: 'value'
@ -653,6 +653,39 @@ define(
});
});
it("selects an range value to display, if available", function () {
mockMetadata.valuesForHints.andReturn([
{
key: 'range',
source: 'range'
}
]);
var key = controller.chooseTelemetryKeyToDisplay(mockMetadata);
expect(key).toEqual('range');
});
it("selects the first non-domain value to display, if no range available", function () {
mockMetadata.valuesForHints.andReturn([]);
mockMetadata.values.andReturn([
{
key: 'domain',
source: 'domain',
hints: {
domain: 1
}
},
{
key: 'image',
source: 'image',
hints: {
image: 1
}
}
]);
var key = controller.chooseTelemetryKeyToDisplay(mockMetadata);
expect(key).toEqual('image');
});
it("reflects limit status", function () {
mockLimitEvaluator.evaluate.andReturn({cssClass: "alarm-a"});
controller.updateView(mockTelemetryObject, [{

View File

@ -49,7 +49,7 @@
{
"key": "ListViewController",
"implementation": ListViewController,
"depends": ["$scope"]
"depends": ["$scope", "formatService"]
}
],
"directives": [

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define(function () {
function ListViewController($scope) {
function ListViewController($scope, formatService) {
this.$scope = $scope;
$scope.orderByField = 'title';
$scope.reverseSort = false;
@ -30,6 +30,8 @@ define(function () {
var unlisten = $scope.domainObject.getCapability('mutation')
.listen(this.updateView.bind(this));
this.utc = formatService.getFormat('utc');
$scope.$on('$destroy', function () {
unlisten();
});
@ -50,17 +52,13 @@ define(function () {
icon: child.getCapability('type').getCssClass(),
title: child.getModel().name,
type: child.getCapability('type').getName(),
persisted: new Date(
child.getModel().persisted
).toUTCString(),
modified: new Date(
child.getModel().modified
).toUTCString(),
persisted: this.utc.format(child.getModel().persisted),
modified: this.utc.format(child.getModel().modified),
asDomainObject: child,
location: child.getCapability('location'),
action: child.getCapability('action')
};
});
}, this);
};
return ListViewController;

View File

@ -31,7 +31,9 @@ define(
controller,
childModel,
typeCapability,
mutationCapability;
mutationCapability,
formatService;
beforeEach(function () {
unlistenFunc = jasmine.createSpy("unlisten");
@ -41,6 +43,18 @@ define(
);
mutationCapability.listen.andReturn(unlistenFunc);
formatService = jasmine.createSpyObj(
"formatService",
["getFormat"]
);
formatService.getFormat.andReturn(jasmine.createSpyObj(
'utc',
["format"]
));
formatService.getFormat().format.andCallFake(function (v) {
return "formatted " + v;
});
typeCapability = jasmine.createSpyObj(
"typeCapability",
["getCssClass", "getName"]
@ -94,20 +108,27 @@ define(
);
scope.domainObject = domainObject;
controller = new ListViewController(scope);
controller = new ListViewController(scope, formatService);
waitsFor(function () {
return scope.children;
});
});
it("uses the UTC time format", function () {
expect(formatService.getFormat).toHaveBeenCalledWith('utc');
});
it("updates the view", function () {
expect(scope.children[0]).toEqual(
{
icon: "icon-folder",
title: "Battery Charge Status",
type: "Folder",
persisted: "Wed, 07 Jun 2017 20:34:57 GMT",
modified: "Wed, 07 Jun 2017 20:34:57 GMT",
persisted: formatService.getFormat('utc')
.format(childModel.persisted),
modified: formatService.getFormat('utc')
.format(childModel.modified),
asDomainObject: childObject,
location: ''
}

View File

@ -170,6 +170,9 @@ define(
* @param rows
*/
TelemetryTableController.prototype.addRowsToTable = function (rows) {
rows.forEach(function (row) {
this.$scope.rows.push(row);
}, this);
this.$scope.$broadcast('add:rows', rows);
};

View File

@ -436,5 +436,28 @@ define(
expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows);
});
describe('when telemetry is added', function () {
var testRows;
var expectedRows;
beforeEach(function () {
testRows = [{ a: 0 }, { a: 1 }, { a: 2 }];
mockScope.rows = [{ a: -1 }];
expectedRows = mockScope.rows.concat(testRows);
spyOn(controller.telemetry, "on").andCallThrough();
controller.registerChangeListeners();
controller.telemetry.on.calls.forEach(function (call) {
if (call.args[0] === 'added') {
call.args[1](testRows);
}
});
});
it("adds it to rows in scope", function () {
expect(mockScope.rows).toEqual(expectedRows);
});
});
});
});

View File

@ -29,6 +29,7 @@ define([
"./src/controllers/TimelineTickController",
"./src/controllers/TimelineTableController",
"./src/controllers/TimelineGanttController",
"./src/controllers/TimelineTOIController",
"./src/controllers/ActivityModeValuesController",
"./src/capabilities/ActivityTimespanCapability",
"./src/capabilities/TimelineTimespanCapability",
@ -37,6 +38,7 @@ define([
"./src/capabilities/CostCapability",
"./src/directives/MCTSwimlaneDrop",
"./src/directives/MCTSwimlaneDrag",
"./src/directives/MCTResourceGraphDrop",
"./src/services/ObjectLoader",
"./src/chart/MCTTimelineChart",
"text!./res/templates/values.html",
@ -59,6 +61,7 @@ define([
TimelineTickController,
TimelineTableController,
TimelineGanttController,
TimelineTOIController,
ActivityModeValuesController,
ActivityTimespanCapability,
TimelineTimespanCapability,
@ -67,6 +70,7 @@ define([
CostCapability,
MCTSwimlaneDrop,
MCTSwimlaneDrag,
MCTResourceGraphDrop,
ObjectLoader,
MCTTimelineChart,
valuesTemplate,
@ -502,6 +506,15 @@ define([
"TIMELINE_MAXIMUM_OFFSCREEN"
]
},
{
"key": "TimelineTOIController",
"implementation": TimelineTOIController,
"depends": [
"openmct",
"timerService",
"$scope"
]
},
{
"key": "ActivityModeValuesController",
"implementation": ActivityModeValuesController,
@ -566,6 +579,13 @@ define([
"$interval",
"$log"
]
},
{
"key": "mctResourceGraphDrop",
"implementation": MCTResourceGraphDrop,
"depends": [
"dndService"
]
}
],
"services": [

View File

@ -29,6 +29,44 @@
}
}
}
// Follow Line
.l-follow-line {
// TODO: move before and after into l-timeline-gantt so those only render in that pane
pointer-events: none;
position: absolute;
top: 0; bottom: 0;
width: 1px;
z-index: 9; // Just below .l-hover-btns-holder
}
}
.l-timeline-gantt {
.l-follow-line {
$d: 0.8rem;
top: $interiorMargin;
&:before,
&:after {
content: '';
display: block;
height: $d;
width: $d;
position: absolute;
top: 0;
@include transform(translateX(-50%));
}
&:before {
// Icon blocker
width: 2 * $d;
}
&:after {
// Icon
font-size: $d;
line-height: $d;
text-align: center;
}
}
}
.s-timeline-gantt {
@ -108,10 +146,9 @@
}
.s-hover-btns-holder {
$bg: $timelineHeaderColorBg;
$bga: 1;
$l: 5%;
@include user-select(none);
@include background-image(linear-gradient(-90deg, rgba($bg, $bga), rgba($bg, $bga) 70%, rgba($bg, 0) 100%));
@include background-image(linear-gradient(-90deg, rgba($bg, 1), rgba($bg, 1) 70%, rgba($bg, 0) 100%));
.s-button {
height: 16px;
line-height: 16px;
@ -129,4 +166,27 @@
color: $timelineResourceGraphFg;
}
}
.s-follow-line {
background: rgba($timeControllerToiLineColor, 0.5);
}
.s-timeline-gantt {
.s-follow-line {
&:after {
// Icon
color: $timeControllerToiLineColor;
content: $glyph-icon-timer;
font-family: symbolsfont;
text-shadow: $shdwItemText;
}
&:before {
// Blocker
$bg: $timelineHeaderColorBg;
$l: 30%;
@include background-image(linear-gradient(90deg, rgba($bg, 0), rgba($bg, 1) $l, rgba($bg, 1) 100% - $l, rgba($bg, 0)));
}
}
}
}

View File

@ -38,6 +38,10 @@
.l-timeline-pane {
@include absPosDefault();
&.drop-over {
background-color: lighten($colorEditAreaBg, 5%);
}
.l-width-control {
position: relative;
}
@ -75,6 +79,10 @@
}
}
&.l-timeline-gantt {
.abs.l-timeline-gantt-header-w {
overflow: hidden;
height: $timelineTopPaneHeaderH;
}
.l-swimlanes-holder {
@include scrollV(scroll);
bottom: $scrollbarTrackSize;

View File

@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false;
@import "../../../../commonUI/general/res/sass/constants";
@import "../../../../commonUI/general/res/sass/mixins";
@import "../../../../commonUI/general/res/sass/glyphs";
@import "../../../../commonUI/themes/espresso/res/sass/constants";
@import "../../../../commonUI/themes/espresso/res/sass/mixins";
@import "constants";

View File

@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false;
@import "../../../../commonUI/general/res/sass/constants";
@import "../../../../commonUI/general/res/sass/mixins";
@import "../../../../commonUI/general/res/sass/glyphs";
@import "../../../../commonUI/themes/snow/res/sass/constants";
@import "../../../../commonUI/themes/snow/res/sass/mixins";
@import "constants";

View File

@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false;
@import "../../../../commonUI/general/res/sass/constants";
@import "../../../../commonUI/general/res/sass/mixins";
@import "../../../../commonUI/general/res/sass/glyphs";
@import "../../../../commonUI/themes/espresso/res/sass/constants";
@import "../../../../commonUI/themes/espresso/res/sass/mixins";
@import "constants";

View File

@ -77,7 +77,8 @@
<mct-splitter></mct-splitter>
<!-- BOTTOM PANE RESOURCE LEGEND -->
<div class="split-pane-component abs l-timeline-pane t-pane-h l-pane-btm s-timeline-resource-legend l-timeline-resource-legend">
<div mct-resource-graph-drop
class="split-pane-component abs l-timeline-pane t-pane-h l-pane-btm s-timeline-resource-legend l-timeline-resource-legend">
<div class="l-title s-title">{{ngModel.title}}Resource Graph Legend</div>
<div class="l-legend-items legend">
<mct-include key="'timeline-legend-item'"
@ -96,109 +97,124 @@
<!-- RIGHT PANE: GANTT AND RESOURCE PLOTS -->
<span ng-controller="TimelineZoomController as zoomController" class="abs">
<mct-split-pane anchor="bottom"
<span class="toi-control-holder temp" ng-controller="TimelineTOIController as toiController">
<mct-split-pane anchor="bottom"
position="pane.y"
class="abs split-pane-component l-timeline-pane l-pane-r t-pane-v">
<!-- TOP PANE GANTT BARS -->
<div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt">
<div class="l-hover-btns-holder s-hover-btns-holder">
<a class="s-button icon-arrows-out"
ng-click="zoomController.fit()"
ng-show="true"
title="Zoom to fit">
</a>
<!-- TOP PANE GANTT BARS -->
<div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt">
<div class="l-hover-btns-holder s-hover-btns-holder">
<a class="s-button icon-timer"
ng-click="scroll.follow = true"
ng-show="!toiController.isFollowing() && toiController.isActive()"
title="Follow time bounds">
</a>
<a class="s-button icon-magnify-in"
ng-click="zoomController.zoom(-1)"
ng-show="true"
title="Zoom in">
</a>
<a class="s-button icon-arrows-out"
ng-click="scroll.follow = false; zoomController.fit()"
ng-show="true"
title="Zoom to fit">
</a>
<a class="s-button icon-magnify-out"
ng-click="zoomController.zoom(1)"
ng-show="true"
title="Zoom out">
</a>
</div>
<a class="s-button icon-magnify-in"
ng-click="scroll.follow = false; zoomController.zoom(-1)"
ng-show="true"
title="Zoom in">
</a>
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
<mct-include key="'timeline-ticks'"
parameters="{
fullWidth: zoomController.width(timelineController.end()),
start: scroll.x,
width: scroll.width,
step: zoomController.toPixels(zoomController.zoom()),
toMillis: zoomController.toMillis
}">
</mct-include>
</div>
<a class="s-button icon-magnify-out"
ng-click="scroll.follow = false; zoomController.zoom(1)"
ng-show="true"
title="Zoom out">
</a>
</div>
<div class="t-swimlanes-holder l-swimlanes-holder"
mct-scroll-x="scroll.x"
mct-scroll-y="scroll.y">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
<div class="t-swimlane s-swimlane l-swimlane"
ng-repeat="swimlane in timelineController.swimlanes()"
ng-class="{
exceeded: swimlane.exceeded(),
selected: selection.selected(swimlane),
'drop-into': swimlane.highlight(),
'drop-after': swimlane.highlightBottom()
}"
ng-click="selection.select(swimlane)"
mct-swimlane-drop="swimlane">
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
<mct-include key="'timeline-ticks'"
parameters="{
fullWidth: zoomController.width(timelineController.end()),
start: scroll.x,
width: scroll.width,
step: zoomController.toPixels(zoomController.zoom()),
toMillis: zoomController.toMillis
}">
</mct-include>
</div>
<div ng-if="toiController.isActive()" class="l-follow-line s-follow-line"
ng-style="{ left: toiController.x() - scroll.x + 'px' }"></div>
<mct-representation key="'gantt'"
mct-object="swimlane.domainObject"
parameters="{
scroll: scroll,
toPixels: zoomController.toPixels
}">
</mct-representation>
<div class="t-swimlanes-holder l-swimlanes-holder"
mct-scroll-x="scroll.x"
mct-scroll-y="scroll.y">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
<div class="t-swimlane s-swimlane l-swimlane"
ng-repeat="swimlane in timelineController.swimlanes()"
ng-class="{
exceeded: swimlane.exceeded(),
selected: selection.selected(swimlane),
'drop-into': swimlane.highlight(),
'drop-after': swimlane.highlightBottom()
}"
ng-click="selection.select(swimlane)"
mct-swimlane-drop="swimlane">
<span ng-if="selection.selected(swimlane)">
<span ng-repeat="handle in timelineController.handles()"
ng-style="handle.style(zoomController)"
style="position: absolute; top: 0px; bottom: 0px;"
class="handle"
ng-class="{ start: $index === 0, mid: $index === 1, end: $index > 1 }"
mct-drag-down="handle.begin()"
mct-drag="handle.drag(delta[0], zoomController); timelineController.refresh()"
mct-drag-up="handle.finish()">
</span>
</span>
<mct-representation key="'gantt'"
mct-object="swimlane.domainObject"
parameters="{
scroll: scroll,
toPixels: zoomController.toPixels
}">
</mct-representation>
<span ng-if="selection.selected(swimlane)">
<span ng-repeat="handle in timelineController.handles()"
ng-style="handle.style(zoomController)"
style="position: absolute; top: 0px; bottom: 0px;"
class="handle"
ng-class="{ start: $index === 0, mid: $index === 1, end: $index > 1 }"
mct-drag-down="handle.begin()"
mct-drag="handle.drag(delta[0], zoomController); timelineController.refresh()"
mct-drag-up="handle.finish()">
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- HORZ SPLITTER -->
<mct-splitter></mct-splitter>
<!-- HORZ SPLITTER -->
<mct-splitter></mct-splitter>
<!-- BOTTOM PANE RESOURCE GRAPHS AND RIGHT PANE HORIZONTAL SCROLL CONTROL -->
<div class="split-pane-component l-timeline-resource-graph l-timeline-pane t-pane-h l-pane-btm">
<div class="l-graphs-holder"
mct-resize="scroll.width = bounds.width">
<div class="t-graphs l-graphs">
<mct-include key="'timeline-resource-graphs'"
parameters="{
origin: zoomController.toMillis(scroll.x),
duration: zoomController.toMillis(scroll.width),
graphs: timelineController.graphs()
}">
</mct-include>
<!-- BOTTOM PANE RESOURCE GRAPHS AND RIGHT PANE HORIZONTAL SCROLL CONTROL -->
<div class="split-pane-component l-timeline-resource-graph l-timeline-pane t-pane-h l-pane-btm">
<div class="l-graphs-holder"
mct-resize="scroll.width = bounds.width">
<div class="t-graphs l-graphs">
<mct-include key="'timeline-resource-graphs'"
parameters="{
origin: zoomController.toMillis(scroll.x),
duration: zoomController.toMillis(scroll.width),
graphs: timelineController.graphs()
}">
</mct-include>
</div>
<div ng-if="toiController.isActive()" class="l-follow-line s-follow-line"
ng-style="{ left: toiController.x() - scroll.x + 'px' }"></div>
</div>
<div mct-scroll-x="scroll.x"
class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
</div>
</div>
</div>
</div>
<div mct-scroll-x="scroll.x"
class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
</div>
</div>
</div>
</mct-split-pane>
</mct-split-pane>
</span>
</span>
</mct-split-pane>
</div>

View File

@ -0,0 +1,111 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Tracks time-of-interest in timelines, updating both scroll state
* (when appropriate) and positioning of the displayed line.
*/
function TimelineTOIController(openmct, timerService, $scope) {
this.openmct = openmct;
this.timerService = timerService;
this.$scope = $scope;
this.change = this.change.bind(this);
this.bounds = this.bounds.bind(this);
this.destroy = this.destroy.bind(this);
this.timerService.on('change', this.change);
this.openmct.time.on('bounds', this.bounds);
this.$scope.$on('$destroy', this.destroy);
this.$scope.scroll.follow = this.timerService.hasTimer();
if (this.$scope.zoomController) {
this.bounds(this.openmct.time.bounds());
}
}
/**
* Handle a `change` event from the timer service; track the
* new timer.
*/
TimelineTOIController.prototype.change = function () {
this.$scope.scroll.follow =
this.$scope.scroll.follow || this.timerService.hasTimer();
};
/**
* Handle a `bounds` event from the time API; scroll the timeline
* to match the current bounds, if currently in follow mode.
*/
TimelineTOIController.prototype.bounds = function (bounds) {
if (this.isFollowing()) {
var start = this.timerService.convert(bounds.start);
var end = this.timerService.convert(bounds.end);
this.duration = bounds.end - bounds.start;
this.$scope.zoomController.bounds(start, end);
}
};
/**
* Handle a `$destroy` event from scope; detach all observers.
*/
TimelineTOIController.prototype.destroy = function () {
this.timerService.off('change', this.change);
this.openmct.time.off('bounds', this.bounds);
};
/**
* Get the x position of the time-of-interest line,
* in pixels from the left edge of the timeline area.
*/
TimelineTOIController.prototype.x = function () {
var now = this.timerService.now();
if (now === undefined) {
return undefined;
}
return this.$scope.zoomController.toPixels(this.timerService.now());
};
/**
* Check if there is an active time-of-interest to be shown.
* @return {boolean} true when active
*/
TimelineTOIController.prototype.isActive = function () {
return this.x() !== undefined;
};
/**
* Check if the timeline should be following time conductor bounds.
* @return {boolean} true when following
*/
TimelineTOIController.prototype.isFollowing = function () {
return !!this.$scope.scroll.follow && this.timerService.now() !== undefined;
};
return TimelineTOIController;
});

View File

@ -32,7 +32,8 @@ define(
// Prefer to start with the middle index
var zoomLevels = ZOOM_CONFIGURATION.levels || [1000],
zoomIndex = Math.floor(zoomLevels.length / 2),
tickWidth = ZOOM_CONFIGURATION.width || 200;
tickWidth = ZOOM_CONFIGURATION.width || 200,
lastWidth = Number.MAX_VALUE; // Don't constrain prematurely
function toMillis(pixels) {
return (pixels / tickWidth) * zoomLevels[zoomIndex];
@ -55,19 +56,29 @@ define(
function setScroll(x) {
$window.requestAnimationFrame(function () {
$scope.scroll.x = x;
$scope.scroll.x = Math.min(
Math.max(x, 0),
lastWidth - $scope.scroll.width
);
$scope.$apply();
});
}
function initializeZoomFromTimespan(timespan) {
var timelineDuration = timespan.getDuration();
function initializeZoomFromStartEnd(start, end) {
var duration = end - start;
zoomIndex = 0;
while (toMillis($scope.scroll.width) < timelineDuration &&
while (toMillis($scope.scroll.width) < duration &&
zoomIndex < zoomLevels.length - 1) {
zoomIndex += 1;
}
setScroll(toPixels(timespan.getStart()));
setScroll(toPixels(start));
}
function initializeZoomFromTimespan(timespan) {
return initializeZoomFromStartEnd(
timespan.getStart(),
timespan.getEnd()
);
}
function initializeZoom() {
@ -101,6 +112,13 @@ define(
}
return zoomLevels[zoomIndex];
},
/**
* Adjust the current zoom bounds to fit both the
* start and the end time provided.
* @param {number} start the starting timestamp
* @param {number} end the ending timestamp
*/
bounds: initializeZoomFromStartEnd,
/**
* Set the zoom level to fit the bounds of the timeline
* being viewed.
@ -119,14 +137,14 @@ define(
*/
toMillis: toMillis,
/**
* Get the pixel width necessary to fit the specified
* timestamp, expressed as an offset in milliseconds from
* the start of the timeline.
* Set the maximum timestamp value to be displayed, and get
* the pixel width necessary to display this value.
* @param {number} timestamp the time to display
*/
width: function (timestamp) {
var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING)));
return Math.max($scope.scroll.width, pixels);
lastWidth = Math.max($scope.scroll.width, pixels);
return lastWidth;
}
};
}

View File

@ -0,0 +1,81 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./SwimlaneDragConstants'],
function (SwimlaneDragConstants) {
/**
* Defines the `mct-resource-graph-drop` directive. When a drop occurs
* on an element with this attribute, the swimlane targeted by the drop
* will receive the dropped domain object (at which point it can handle
* the drop, typically by toggling the swimlane graph.)
* @param {DndService} dndService drag-and-drop service
*/
function MCTResourceGraphDrop(dndService) {
function link(scope, element, attrs) {
// Handle dragover
element.on('dragover', function (e) {
var swimlane = dndService.getData(
SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
);
if (typeof swimlane !== "undefined" && !swimlane.graph()) {
element.addClass('drop-over');
scope.$apply();
e.preventDefault();
}
});
// Handle drops
element.on('drop', function (e) {
var swimlane = dndService.getData(
SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
);
element.removeClass('drop-over');
// Only toggle if the graph isn't already set
if (typeof swimlane !== "undefined" && !swimlane.graph()) {
swimlane.toggleGraph();
e.preventDefault();
}
});
// Clear highlights when drag leaves this swimlane
element.on('dragleave', function (e) {
element.removeClass('drop-over');
scope.$apply();
e.preventDefault();
});
}
return {
// Applies to attributes
restrict: "A",
// Link using above function
link: link
};
}
return MCTResourceGraphDrop;
}
);

View File

@ -0,0 +1,138 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"../../src/controllers/TimelineTOIController",
"EventEmitter"
], function (TimelineTOIController, EventEmitter) {
describe("The timeline TOI controller", function () {
var mockmct;
var mockTimerService;
var mockScope;
var controller;
beforeEach(function () {
mockmct = { time: new EventEmitter() };
mockmct.time.bounds = jasmine.createSpy('bounds');
mockTimerService = new EventEmitter();
mockTimerService.getTimer = jasmine.createSpy('getTimer');
mockTimerService.hasTimer = jasmine.createSpy('hasTimer');
mockTimerService.now = jasmine.createSpy('now');
mockTimerService.convert = jasmine.createSpy('convert');
mockScope = new EventEmitter();
mockScope.$on = mockScope.on.bind(mockScope);
mockScope.zoomController = jasmine.createSpyObj('zoom', [
'bounds',
'toPixels'
]);
mockScope.scroll = { x: 10, width: 1000 };
spyOn(mockmct.time, "on").andCallThrough();
spyOn(mockmct.time, "off").andCallThrough();
spyOn(mockTimerService, "on").andCallThrough();
spyOn(mockTimerService, "off").andCallThrough();
controller = new TimelineTOIController(
mockmct,
mockTimerService,
mockScope
);
});
it("reports an undefined x position initially", function () {
expect(controller.x()).toBeUndefined();
});
it("listens for bounds changes", function () {
expect(mockmct.time.on)
.toHaveBeenCalledWith('bounds', controller.bounds);
});
it("listens for timer changes", function () {
expect(mockTimerService.on)
.toHaveBeenCalledWith('change', controller.change);
});
it("is not active", function () {
expect(controller.isActive()).toBe(false);
});
describe("on $destroy from scope", function () {
beforeEach(function () {
mockScope.emit("$destroy");
});
it("unregisters listeners", function () {
expect(mockmct.time.off)
.toHaveBeenCalledWith('bounds', controller.bounds);
expect(mockTimerService.off)
.toHaveBeenCalledWith('change', controller.change);
});
});
describe("when a timer and timestamp present", function () {
var mockTimer;
var testNow;
beforeEach(function () {
testNow = 333221;
mockScope.zoomController.toPixels
.andCallFake(function (millis) {
return millis * 2;
});
mockTimerService.emit('change', mockTimer);
mockTimerService.now.andReturn(testNow);
});
it("reports an x value from the zoomController", function () {
var now = mockTimerService.now();
var expected = mockScope.zoomController.toPixels(now);
expect(controller.x()).toEqual(expected);
});
});
describe("when follow mode is disabled", function () {
beforeEach(function () {
mockScope.scroll.follow = false;
});
it("ignores bounds events", function () {
mockmct.time.emit('bounds', { start: 0, end: 1000 });
expect(mockScope.zoomController.bounds)
.not.toHaveBeenCalled();
});
});
describe("when follow mode is enabled", function () {
beforeEach(function () {
mockScope.scroll.follow = true;
mockTimerService.now.andReturn(500);
});
it("zooms on bounds events", function () {
mockmct.time.emit('bounds', { start: 0, end: 1000 });
expect(mockScope.zoomController.bounds)
.toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1,159 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../../src/directives/MCTResourceGraphDrop', '../../src/directives/SwimlaneDragConstants'],
function (MCTResourceGraphDrop, SwimlaneDragConstants) {
describe("The mct-resource-graph-drop directive", function () {
var mockDndService,
mockScope,
mockElement,
testAttrs,
mockSwimlane,
testEvent,
handlers,
directive;
beforeEach(function () {
handlers = {};
mockDndService = jasmine.createSpyObj(
'dndService',
['setData', 'getData', 'removeData']
);
mockScope = jasmine.createSpyObj('$scope', ['$eval', '$apply']);
mockElement = jasmine.createSpyObj('element', ['on', 'addClass', 'removeClass']);
testAttrs = { mctSwimlaneDrop: "mockSwimlane" };
mockSwimlane = jasmine.createSpyObj(
"swimlane",
['graph', 'toggleGraph']
);
testEvent = {
dataTransfer: { getData: jasmine.createSpy() },
preventDefault: jasmine.createSpy(),
stopPropagation: jasmine.createSpy()
};
testEvent.dataTransfer.getData.andReturn('abc');
mockDndService.getData.andCallFake(function (key) {
return key === SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE ?
mockSwimlane : undefined;
});
mockSwimlane.graph.andReturn(false);
directive = new MCTResourceGraphDrop(mockDndService);
directive.link(mockScope, mockElement, testAttrs);
mockElement.on.calls.forEach(function (call) {
handlers[call.args[0]] = call.args[1];
});
});
it("is available as an attribute", function () {
expect(directive.restrict).toEqual("A");
});
[false, true].forEach(function (graphing) {
describe("when swimlane graph is " + (graphing ? "" : "not ") + "enabled", function () {
beforeEach(function () {
mockSwimlane.graph.andReturn(graphing);
});
describe("on dragover", function () {
var prefix = !graphing ? "does" : "does not";
beforeEach(function () {
handlers.dragover(testEvent);
});
it(prefix + " add a drop-over class", function () {
var expectAddClass = expect(mockElement.addClass);
(!graphing ? expectAddClass : expectAddClass.not)
.toHaveBeenCalledWith('drop-over');
});
it(prefix + " call $apply on scope", function () {
var expectApply = expect(mockScope.$apply);
(!graphing ? expectApply : expectApply.not)
.toHaveBeenCalled();
});
it(prefix + " prevent default", function () {
var expectPreventDefault = expect(testEvent.preventDefault);
(!graphing ? expectPreventDefault : expectPreventDefault.not)
.toHaveBeenCalled();
});
});
describe("on drop", function () {
var prefix = !graphing ? "does" : "does not";
beforeEach(function () {
handlers.drop(testEvent);
});
it("removes any drop-over class", function () {
expect(mockElement.removeClass)
.toHaveBeenCalledWith('drop-over');
});
it(prefix + " toggle the swimlane's resource graph", function () {
var expectToggle = expect(mockSwimlane.toggleGraph);
(!graphing ? expectToggle : expectToggle.not)
.toHaveBeenCalled();
});
it(prefix + " prevent default", function () {
var expectPreventDefault = expect(testEvent.preventDefault);
(!graphing ? expectPreventDefault : expectPreventDefault.not)
.toHaveBeenCalled();
});
});
describe("on dragleave", function () {
beforeEach(function () {
handlers.dragleave(testEvent);
});
it("removes any drop-over class", function () {
expect(mockElement.removeClass)
.toHaveBeenCalledWith('drop-over');
});
it("calls $apply on scope", function () {
expect(mockScope.$apply).toHaveBeenCalled();
});
it("calls preventDefault on events", function () {
expect(testEvent.preventDefault).toHaveBeenCalled();
});
});
});
});
});
}
);

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

@ -27,7 +27,7 @@
ng-required="ngRequired || compositeCtrl.isNonEmpty(ngModel[field])"
ng-pattern="ngPattern"
options="item.options"
structure="row"
structure="item"
field="$index">
</mct-control>
<span class="composite-control-label">

View File

@ -50,11 +50,18 @@ define([
this.rootProvider = new RootObjectProvider(this.rootRegistry);
}
/**
* Set fallback provider, this is an internal API for legacy reasons.
* @private
*/
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
// Retrieve the provider for a given key.
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
if (identifier.key === 'ROOT') {
return this.rootProvider;
@ -135,27 +142,28 @@ define([
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
identifier = utils.parseKeyString(identifier);
var provider = this.getProvider(identifier);
[
'save',
'delete',
'get'
].forEach(function (method) {
ObjectAPI.prototype[method] = function () {
var identifier = arguments[0],
provider = this.getProvider(identifier);
if (!provider) {
throw new Error('No Provider Matched');
}
if (!provider) {
throw new Error('No Provider Matched');
}
if (!provider.get) {
throw new Error('Provider does not support get!');
}
if (!provider[method]) {
throw new Error('Provider does not support [' + method + '].');
}
return provider.get(identifier);
};
return provider[method].apply(provider, arguments);
};
});
ObjectAPI.prototype.delete = function () {
throw new Error('Delete not implemented');
};
ObjectAPI.prototype.save = function () {
throw new Error('Save not implemented');
};
/**
* Add a root-level object.

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

@ -0,0 +1,97 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
// Set of connection states; changing among these states will be
// reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write.
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
// PENDING: Still trying to connect, and haven't failed yet.
var CONNECTED = {
glyphClass: "ok"
},
PENDING = {
glyphClass: 'caution'
},
DISCONNECTED = {
glyphClass: "err"
};
function URLIndicator($http, $interval) {
var self = this;
this.cssClass = this.options.cssClass ? this.options.cssClass : "icon-database";
this.URLpath = this.options.url;
this.label = this.options.label ? this.options.label : this.options.url;
this.interval = this.options.interval || 10000;
this.state = PENDING;
function handleError(e) {
self.state = DISCONNECTED;
}
function handleResponse() {
self.state = CONNECTED;
}
function updateIndicator() {
$http.get(self.URLpath).then(handleResponse, handleError);
}
updateIndicator();
$interval(updateIndicator, self.interval, 0, false);
}
URLIndicator.prototype.getCssClass = function () {
return this.cssClass;
};
URLIndicator.prototype.getGlyphClass = function () {
return this.state.glyphClass;
};
URLIndicator.prototype.getText = function () {
switch (this.state) {
case CONNECTED: {
return this.label + " is connected";
}
case PENDING: {
return "Checking status of " + this.label + " please stand by...";
}
case DISCONNECTED: {
return this.label + " is offline";
}
}
};
URLIndicator.prototype.getDescription = function () {
switch (this.state) {
case CONNECTED: {
return this.label + " is online, checking status every " +
this.interval + " milliseconds.";
}
case PENDING: {
return "Checking status of " + this.label + " please stand by...";
}
case DISCONNECTED: {
return this.label + " is offline, checking status every " +
this.interval + " milliseconds";
}
}
};
return URLIndicator;
});

View File

@ -0,0 +1,20 @@
define(
[
'./URLIndicator'
],
function URLIndicatorPlugin(URLIndicator) {
return function (opts) {
// Wrap the plugin in a function so we can apply the arguments.
function URLIndicatorWrapper() {
this.options = opts;
URLIndicator.apply(this, arguments);
}
URLIndicatorWrapper.prototype = Object.create(URLIndicator.prototype);
return function install(openmct) {
openmct.legacyExtension('indicators', {
"implementation": URLIndicatorWrapper,
"depends": ["$http", "$interval"]
});
};
};
});

View File

@ -0,0 +1,158 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["./URLIndicator"],
function (URLIndicator) {
describe("The URLIndicator", function () {
var mockHttp,
mockInterval,
mockPromise,
opts,
Indicator,
indicatorWrapper;
beforeEach(function () {
mockHttp = jasmine.createSpyObj("$http", ["get"]);
mockInterval = jasmine.createSpy("$interval");
mockPromise = jasmine.createSpyObj("promise", ["then"]);
opts = {
url: "http://localhost:8080",
interval: 1337 //some number
};
mockHttp.get.andReturn(mockPromise);
Indicator = function () {
this.options = opts;
URLIndicator.call(this, mockHttp, mockInterval);
};
Indicator.prototype = Object.create(URLIndicator.prototype);
indicatorWrapper = new Indicator();
});
it("polls for changes", function () {
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
opts.interval,
0,
false
);
});
it("has a database cssClass as default", function () {
expect(indicatorWrapper.getCssClass()).toEqual("icon-database");
});
it("consults the url with the path supplied", function () {
expect(mockHttp.get).toHaveBeenCalledWith(opts.url);
});
it("changes when the database connection is nominal", function () {
var initialText = indicatorWrapper.getText(),
initialDescrption = indicatorWrapper.getDescription(),
initialGlyphClass = indicatorWrapper.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.mostRecentCall.args[0]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicatorWrapper.getText()).not.toEqual(initialText);
expect(indicatorWrapper.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicatorWrapper.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicatorWrapper.getGlyphClass()).toEqual("ok");
});
it("changes when the server cannot be reached", function () {
var initialText = indicatorWrapper.getText(),
initialDescrption = indicatorWrapper.getDescription(),
initialGlyphClass = indicatorWrapper.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.mostRecentCall.args[1]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicatorWrapper.getText()).not.toEqual(initialText);
expect(indicatorWrapper.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicatorWrapper.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicatorWrapper.getGlyphClass()).toEqual("err");
});
it("has a customized cssClass if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
cssClass: "cssClass-checked",
interval: 10000
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getCssClass()).toEqual("cssClass-checked");
});
it("has a customized interval if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
interval: 1814
};
indicatorWrapper = new Indicator();
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
1814,
0,
false
);
});
it("has a custom label if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
label: "Localhost"
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getText()).toEqual("Checking status of Localhost please stand by...");
});
it("has a default label if not supplied in initialization", function () {
opts = {
url: "http://localhost:8080"
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getText()).toEqual(
"Checking status of http://localhost:8080 please stand by..."
);
});
it("has a default interval if not supplied in initialization", function () {
opts = {
url: "http://localhost:8080"
};
indicatorWrapper = new Indicator();
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
10000,
0,
false
);
});
});
}
);

View File

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

View File

@ -0,0 +1,44 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
function SummaryWidgetsCompositionPolicy(openmct) {
this.openmct = openmct;
}
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {
var parentType = parent.getCapability('type');
var newStyleChild = child.useCapability('adapter');
if (parentType.instanceOf('summary-widget') && !this.openmct.telemetry.canProvideTelemetry(newStyleChild)) {
return false;
}
return true;
};
return SummaryWidgetsCompositionPolicy;
}
);

View File

@ -0,0 +1,67 @@
define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (SummaryWidget, SummaryWidgetsCompositionPolicy) {
function plugin() {
var widgetType = {
name: 'Summary Widget',
description: 'A compact status update for collections of telemetry-producing items',
creatable: true,
cssClass: 'icon-summary-widget',
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
domainObject.openNewTab = 'thisTab';
},
form: [
{
"key": "url",
"name": "URL",
"control": "textfield",
"pattern": "^(ftp|https?)\\:\\/\\/",
"required": false,
"cssClass": "l-input-lg"
},
{
"key": "openNewTab",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"value": "thisTab",
"name": "Open in this tab"
},
{
"value": "newTab",
"name": "Open in a new tab"
}
],
"cssClass": "l-inline"
}
]
};
function initViewProvider(openmct) {
return {
name: 'Widget View',
view: function (domainObject) {
return new SummaryWidget(domainObject, openmct);
},
canView: function (domainObject) {
return (domainObject.type === 'summary-widget');
},
editable: true,
key: 'summaryWidgets'
};
}
return function install(openmct) {
openmct.types.addType('summary-widget', widgetType);
openmct.objectViews.addProvider(initViewProvider(openmct));
openmct.legacyExtension('policies', {category: 'composition',
implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct']
});
};
}
return plugin;
});

View File

@ -0,0 +1,11 @@
<li class="t-condition">
<label class="t-condition-context">when</label>
<span class="controls">
<span class="t-configuration"> </span>
<span class="t-value-inputs"> </span>
</span>
<span class="flex-elem l-condition-action-buttons-wrapper">
<a class="s-icon-button icon-duplicate t-duplicate" title="Duplicate this condition"></a>
<a class="s-icon-button icon-trash t-delete" title="Delete this condition"></a>
</span>
</li>

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

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" title="Duplicate this test value"></a>
<a class="s-icon-button icon-trash t-delete" title="Delete this test value"></a>
</span>
</li>
</ul>
</div>

View File

@ -0,0 +1,15 @@
<div class="flex-accordion-holder">
<div class="flex-accordion-holder t-widget-test-data-content w-widget-test-data-content">
<div class="l-enable">
<label class="checkbox custom">Apply Test Values
<input type="checkbox" class="t-test-data-checkbox">
<em></em>
</label>
</div>
<div class="t-test-data-config w-widget-test-data-items">
<div class="holder add-rule-button-wrapper align-right">
<a id="addRule" class="e-control s-button major labeled add-test-condition icon-plus">Add Test Value</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div class="w-summary-widget s-status-no-data">
<a id="widget" class="t-summary-widget l-summary-widget s-summary-widget labeled">
<span id="widgetLabel" class="label widget-label">Default Static Name</span>
</a>
<div class="holder flex-elem t-message-inline l-message message-severity-alert t-message-widget-no-data">
<div class="w-message-contents l-message-body-only">
<div class="message-body">
You must add at least one telemetry object to edit this widget.
</div>
</div>
</div>
<div class="holder l-flex-col l-flex-accordion flex-elem grows widget-edit-holder expanded-widget-test-data expanded-widget-rules">
<div class="section-header"><span class="view-control t-view-control-test-data expanded"></span>Test Data Values</div>
<div class="widget-test-data flex-accordion-holder"></div>
<div class="section-header"><span class="view-control t-view-control-rules expanded"></span>Rules</div>
<div class="holder widget-rules-wrapper flex-elem expanded">
<div id="ruleArea" class="widget-rules"></div>
<div class="holder add-rule-button-wrapper align-right">
<a id="addRule" class="s-button major labeled add-rule-button icon-plus">Add Rule</a>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,402 @@
define([
'text!../res/widgetTemplate.html',
'./Rule',
'./ConditionManager',
'./TestDataManager',
'./WidgetDnD',
'lodash',
'zepto'
], function (
widgetTemplate,
Rule,
ConditionManager,
TestDataManager,
WidgetDnD,
_,
$
) {
//default css configuration for new rules
var DEFAULT_PROPS = {
'color': '#ffffff',
'background-color': '#38761d',
'border-color': 'rgba(0,0,0,0)'
};
/**
* A Summary Widget object, which allows a user to configure rules based
* on telemetry producing domain objects, and update a compact display
* accordingly.
* @constructor
* @param {Object} domainObject The domain Object represented by this Widget
* @param {MCT} openmct An MCT instance
*/
function SummaryWidget(domainObject, openmct) {
this.domainObject = domainObject;
this.openmct = openmct;
this.domainObject.configuration = this.domainObject.configuration || {};
this.domainObject.configuration.ruleConfigById = this.domainObject.configuration.ruleConfigById || {};
this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || ['default'];
this.domainObject.configuration.testDataConfig = this.domainObject.configuration.testDataConfig || [{
object: '',
key: '',
value: ''
}];
this.activeId = 'default';
this.rulesById = {};
this.domElement = $(widgetTemplate);
this.toggleRulesControl = $('.t-view-control-rules', this.domElement);
this.toggleTestDataControl = $('.t-view-control-test-data', this.domElement);
this.widgetButton = this.domElement.children('#widget');
this.editing = false;
this.container = '';
this.editListenerUnsubscribe = $.noop;
this.outerWrapper = $('.widget-edit-holder', this.domElement);
this.ruleArea = $('#ruleArea', this.domElement);
this.configAreaRules = $('.widget-rules-wrapper', this.domElement);
this.testDataArea = $('.widget-test-data', this.domElement);
this.addRuleButton = $('#addRule', this.domElement);
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.testDataManager = new TestDataManager(this.domainObject, this.conditionManager, this.openmct);
this.watchForChanges = this.watchForChanges.bind(this);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.addRule = this.addRule.bind(this);
this.onEdit = this.onEdit.bind(this);
this.addHyperlink(domainObject.url, domainObject.openNewTab);
this.watchForChanges(openmct, domainObject);
var id = this.domainObject.identifier.key,
self = this,
oldDomainObject,
statusCapability;
/**
* Toggles the configuration area for test data in the view
* @private
*/
function toggleTestData() {
self.outerWrapper.toggleClass('expanded-widget-test-data');
self.toggleTestDataControl.toggleClass('expanded');
}
this.toggleTestDataControl.on('click', toggleTestData);
/**
* Toggles the configuration area for rules in the view
* @private
*/
function toggleRules() {
self.outerWrapper.toggleClass('expanded-widget-rules');
self.toggleRulesControl.toggleClass('expanded');
}
this.toggleRulesControl.on('click', toggleRules);
openmct.$injector.get('objectService')
.getObjects([id])
.then(function (objs) {
oldDomainObject = objs[id];
statusCapability = oldDomainObject.getCapability('status');
self.editListenerUnsubscribe = statusCapability.listen(self.onEdit);
if (statusCapability.get('editing')) {
self.onEdit(['editing']);
} else {
self.onEdit([]);
}
});
}
/**
* adds or removes href to widget button and adds or removes openInNewTab
* @param {string} url String that denotes the url to be opened
* @param {string} openNewTab String that denotes wether to open link in new tab or not
*/
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) {
this.widgetButton.attr('href', url);
} else {
this.widgetButton.removeAttr('href');
}
if (openNewTab === 'newTab') {
this.widgetButton.attr('target', '_blank');
} else {
this.widgetButton.removeAttr('target');
}
};
/**
* adds a listener to the object to watch for any changes made by user
* only executes if changes are observed
* @param {openmct} Object Instance of OpenMCT
* @param {domainObject} Object instance of this object
*/
SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) {
this.watchForChangesUnsubscribe = openmct.objects.observe(domainObject, '*', function (newDomainObject) {
if (newDomainObject.url !== this.domainObject.url ||
newDomainObject.openNewTab !== this.domainObject.openNewTab) {
this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab);
}
}.bind(this));
};
/**
* Builds the Summary Widget's DOM, performs other necessary setup, and attaches
* this Summary Widget's view to the supplied container.
* @param {element} container The DOM element that will contain this Summary
* Widget's view.
*/
SummaryWidget.prototype.show = function (container) {
var self = this;
this.container = container;
$(container).append(this.domElement);
$('.widget-test-data', this.domElement).append(this.testDataManager.getDOM());
this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById);
this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
self.initRule(ruleId);
});
this.refreshRules();
this.updateWidget();
this.updateView();
this.addRuleButton.on('click', this.addRule);
this.conditionManager.on('receiveTelemetry', this.executeRules, this);
this.widgetDnD.on('drop', this.reorder, this);
};
/**
* Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry,
* and clean up event handlers
*/
SummaryWidget.prototype.destroy = function (container) {
this.editListenerUnsubscribe();
this.conditionManager.destroy();
this.widgetDnD.destroy();
this.watchForChangesUnsubscribe();
Object.values(this.rulesById).forEach(function (rule) {
rule.destroy();
});
};
/**
* A callback function for the Open MCT status capability listener. If the
* view representing the domain object is in edit mode, update the internal
* state and widget view accordingly.
* @param {string[]} status an array containing the domain object's current status
*/
SummaryWidget.prototype.onEdit = function (status) {
if (status && status.includes('editing')) {
this.editing = true;
} else {
this.editing = false;
}
this.updateView();
};
/**
* If this view is currently in edit mode, show all rule configuration interfaces.
* Otherwise, hide them.
*/
SummaryWidget.prototype.updateView = function () {
if (this.editing) {
this.ruleArea.show();
this.testDataArea.show();
this.addRuleButton.show();
} else {
this.ruleArea.hide();
this.testDataArea.hide();
this.addRuleButton.hide();
}
};
/**
* Update the view from the current rule configuration and order
*/
SummaryWidget.prototype.refreshRules = function () {
var self = this,
ruleOrder = self.domainObject.configuration.ruleOrder,
rules = self.rulesById;
self.ruleArea.html('');
Object.values(ruleOrder).forEach(function (ruleId) {
self.ruleArea.append(rules[ruleId].getDOM());
});
this.executeRules();
this.addOrRemoveDragIndicator();
};
SummaryWidget.prototype.addOrRemoveDragIndicator = function () {
var rules = this.domainObject.configuration.ruleOrder;
var rulesById = this.rulesById;
rules.forEach(function (ruleKey, index, array) {
if (array.length > 2 && index > 0) {
$('.t-grippy', rulesById[ruleKey].domElement).show();
} else {
$('.t-grippy', rulesById[ruleKey].domElement).hide();
}
});
};
/**
* Update the widget's appearance from the configuration of the active rule
*/
SummaryWidget.prototype.updateWidget = function () {
var activeRule = this.rulesById[this.activeId];
this.applyStyle($('#widget', this.domElement), activeRule.getProperty('style'));
$('#widget', this.domElement).prop('title', activeRule.getProperty('message'));
$('#widgetLabel', this.domElement).html(activeRule.getProperty('label'));
$('#widgetLabel', this.domElement).removeClass().addClass('label widget-label ' + activeRule.getProperty('icon'));
};
/**
* Get the active rule and update the Widget's appearance.
*/
SummaryWidget.prototype.executeRules = function () {
this.activeId = this.conditionManager.executeRules(
this.domainObject.configuration.ruleOrder,
this.rulesById
);
this.updateWidget();
};
/**
* Add a new rule to this widget
*/
SummaryWidget.prototype.addRule = function () {
var ruleCount = 0,
ruleId,
ruleOrder = this.domainObject.configuration.ruleOrder;
while (Object.keys(this.rulesById).includes('rule' + ruleCount)) {
ruleCount++;
}
ruleId = 'rule' + ruleCount;
ruleOrder.push(ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
this.initRule(ruleId, 'Rule');
this.refreshRules();
};
/**
* Duplicate an existing widget rule from its configuration and splice it in
* after the rule it duplicates
* @param {Object} sourceConfig The configuration properties of the rule to be
* instantiated
*/
SummaryWidget.prototype.duplicateRule = function (sourceConfig) {
var ruleCount = 0,
ruleId,
sourceRuleId = sourceConfig.id,
ruleOrder = this.domainObject.configuration.ruleOrder,
ruleIds = Object.keys(this.rulesById);
while (ruleIds.includes('rule' + ruleCount)) {
ruleCount = ++ruleCount;
}
ruleId = 'rule' + ruleCount;
sourceConfig.id = ruleId;
sourceConfig.name += ' Copy';
ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig;
this.updateDomainObject();
this.initRule(ruleId, sourceConfig.name);
this.refreshRules();
};
/**
* Initialze a new rule from a default configuration, or build a {Rule} object
* from it if already exists
* @param {string} ruleId An key to be used to identify this ruleId, or the key
of the rule to be instantiated
* @param {string} ruleName The initial human-readable name of this rule
*/
SummaryWidget.prototype.initRule = function (ruleId, ruleName) {
var ruleConfig,
styleObj = {};
Object.assign(styleObj, DEFAULT_PROPS);
if (!this.domainObject.configuration.ruleConfigById[ruleId]) {
this.domainObject.configuration.ruleConfigById[ruleId] = {
name: ruleName || 'Rule',
label: 'Unnamed Rule',
message: '',
id: ruleId,
icon: ' ',
style: styleObj,
description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule',
conditions: [{
object: '',
key: '',
operation: '',
values: []
}],
jsCondition: '',
trigger: 'any',
expanded: 'true'
};
}
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct,
this.conditionManager, this.widgetDnD, this.container);
this.rulesById[ruleId].on('remove', this.refreshRules, this);
this.rulesById[ruleId].on('duplicate', this.duplicateRule, this);
this.rulesById[ruleId].on('change', this.updateWidget, this);
this.rulesById[ruleId].on('conditionChange', this.executeRules, this);
};
/**
* Given two ruleIds, move the source rule after the target rule and update
* the view.
* @param {Object} event An event object representing this drop with draggingId
* and dropTarget fields
*/
SummaryWidget.prototype.reorder = function (event) {
var ruleOrder = this.domainObject.configuration.ruleOrder,
sourceIndex = ruleOrder.indexOf(event.draggingId),
targetIndex;
if (event.draggingId !== event.dropTarget) {
ruleOrder.splice(sourceIndex, 1);
targetIndex = ruleOrder.indexOf(event.dropTarget);
ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
}
this.refreshRules();
};
/**
* Apply a list of css properties to an element
* @param {element} elem The DOM element to which the rules will be applied
* @param {object} style an object representing the style
*/
SummaryWidget.prototype.applyStyle = function (elem, style) {
Object.keys(style).forEach(function (propId) {
elem.css(propId, style[propId]);
});
};
/**
* Mutate this domain object's configuration with the current local configuration
*/
SummaryWidget.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration);
};
return SummaryWidget;
});

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,190 @@
define([
'text!../res/testDataTemplate.html',
'./TestDataItem',
'zepto',
'lodash'
], function (
testDataTemplate,
TestDataItem,
$,
_
) {
/**
* Controls the input and usage of test data in the summary widget.
* @constructor
* @param {Object} domainObject The summary widget domain object
* @param {ConditionManager} conditionManager A conditionManager instance
* @param {MCT} openmct and MCT instance
*/
function TestDataManager(domainObject, conditionManager, openmct) {
var self = this;
this.domainObject = domainObject;
this.manager = conditionManager;
this.openmct = openmct;
this.evaluator = this.manager.getEvaluator();
this.domElement = $(testDataTemplate);
this.config = this.domainObject.configuration.testDataConfig;
this.testCache = {};
this.itemArea = $('.t-test-data-config', this.domElement);
this.addItemButton = $('.add-test-condition', this.domElement);
this.testDataInput = $('.t-test-data-checkbox', this.domElement);
/**
* Toggles whether the associated {ConditionEvaluator} uses the actual
* subscription cache or the test data cache
* @param {Event} event The change event that triggered this callback
* @private
*/
function toggleTestData(event) {
var elem = event.target;
self.evaluator.useTestData(elem.checked);
self.updateTestCache();
}
this.addItemButton.on('click', function () {
self.initItem();
});
this.testDataInput.on('change', toggleTestData);
this.evaluator.setTestDataCache(this.testCache);
this.evaluator.useTestData(false);
this.refreshItems();
}
/**
* Get the DOM element representing this test data manager in the view
*/
TestDataManager.prototype.getDOM = function () {
return this.domElement;
};
/**
* Initialze a new test data item, either from a source configuration, or with
* the default empty configuration
* @param {Object} [config] An object with sourceItem and index fields to instantiate
* this rule from, optional
*/
TestDataManager.prototype.initItem = function (config) {
var sourceIndex = config && config.index,
defaultItem = {
object: '',
key: '',
value: ''
},
newItem;
newItem = (config !== undefined ? config.sourceItem : defaultItem);
if (sourceIndex !== undefined) {
this.config.splice(sourceIndex + 1, 0, newItem);
} else {
this.config.push(newItem);
}
this.updateDomainObject();
this.refreshItems();
};
/**
* Remove an item from this TestDataManager at the given index
* @param {number} removeIndex The index of the item to remove
*/
TestDataManager.prototype.removeItem = function (removeIndex) {
_.remove(this.config, function (item, index) {
return index === removeIndex;
});
this.updateDomainObject();
this.refreshItems();
};
/**
* Change event handler for the test data items which compose this
* test data generateor
* @param {Object} event An object representing this event, with value, property,
* and index fields
*/
TestDataManager.prototype.onItemChange = function (event) {
this.config[event.index][event.property] = event.value;
this.updateDomainObject();
this.updateTestCache();
};
/**
* Builds the test cache from the current item configuration, and passes
* the new test cache to the associated {ConditionEvaluator} instance
*/
TestDataManager.prototype.updateTestCache = function () {
this.generateTestCache();
this.evaluator.setTestDataCache(this.testCache);
this.manager.triggerTelemetryCallback();
};
/**
* Intantiate {TestDataItem} objects from the current configuration, and
* update the view accordingly
*/
TestDataManager.prototype.refreshItems = function () {
var self = this;
self.items = [];
$('.t-test-data-item', this.domElement).remove();
this.config.forEach(function (item, index) {
var newItem = new TestDataItem(item, index, self.manager);
newItem.on('remove', self.removeItem, self);
newItem.on('duplicate', self.initItem, self);
newItem.on('change', self.onItemChange, self);
self.items.push(newItem);
});
self.items.forEach(function (item) {
// $('li:last-of-type', self.itemArea).before(item.getDOM());
self.itemArea.prepend(item.getDOM());
});
if (self.items.length === 1) {
self.items[0].hideButtons();
}
this.updateTestCache();
};
/**
* Builds a test data cache in the format of a telemetry subscription cache
* as expected by a {ConditionEvaluator}
*/
TestDataManager.prototype.generateTestCache = function () {
var testCache = this.testCache,
manager = this.manager,
compositionObjs = manager.getComposition(),
metadata;
testCache = {};
Object.keys(compositionObjs).forEach(function (id) {
testCache[id] = {};
metadata = manager.getTelemetryMetadata(id);
Object.keys(metadata).forEach(function (key) {
testCache[id][key] = '';
});
});
this.config.forEach(function (item) {
if (testCache[item.object]) {
testCache[item.object][item.key] = item.value;
}
});
this.testCache = testCache;
};
/**
* Update the domain object configuration associated with this test data manager
*/
TestDataManager.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config);
};
return TestDataManager;
});

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

View File

@ -0,0 +1,372 @@
define(['../src/ConditionManager'], function (ConditionManager) {
describe('A Summary Widget Condition Manager', function () {
var conditionManager,
mockDomainObject,
mockCompObject1,
mockCompObject2,
mockCompObject3,
mockMetadata,
mockTelemetryCallbacks,
mockEventCallbacks,
unsubscribeSpies,
unregisterSpies,
mockMetadataManagers,
mockComposition,
mockOpenMCT,
mockTelemetryAPI,
addCallbackSpy,
loadCallbackSpy,
removeCallbackSpy,
telemetryCallbackSpy,
metadataCallbackSpy,
mockTelemetryValues,
mockTelemetryValues2,
mockConditionEvaluator;
beforeEach(function () {
mockDomainObject = {
identifier: {
key: 'testKey'
},
name: 'Test Object',
composition: [{
mockCompObject1: {
key: 'mockCompObject1'
},
mockCompObject2 : {
key: 'mockCompObject2'
}
}],
configuration: {}
};
mockCompObject1 = {
identifier: {
key: 'mockCompObject1'
},
name: 'Object 1'
};
mockCompObject2 = {
identifier: {
key: 'mockCompObject2'
},
name: 'Object 2'
};
mockCompObject3 = {
identifier: {
key: 'mockCompObject3'
},
name: 'Object 3'
};
mockMetadata = {
mockCompObject1: {
property1: {
key: 'property1',
name: 'Property 1'
},
property2: {
key: 'property2',
name: 'Property 2'
}
},
mockCompObject2: {
property3: {
key: 'property3',
name: 'Property 3'
},
property4: {
key: 'property4',
name: 'Property 4'
}
},
mockCompObject3: {
property1: {
key: 'property1',
name: 'Property 1'
},
property2: {
key: 'property2',
name: 'Property 2'
}
}
};
mockTelemetryCallbacks = {};
mockEventCallbacks = {};
unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [
'mockCompObject1',
'mockCompObject2',
'mockCompObject3'
]);
unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', [
'load',
'remove',
'add'
]);
mockTelemetryValues = {
mockCompObject1: {
property1: 'Its a string',
property2: 42
},
mockCompObject2: {
property3: 'Execute order:',
property4: 66
},
mockCompObject3: {
property1: 'Testing 1 2 3',
property2: 9000
}
};
mockTelemetryValues2 = {
mockCompObject1: {
property1: 'Its a different string',
property2: 44
},
mockCompObject2: {
property3: 'Execute catch:',
property4: 22
},
mockCompObject3: {
property1: 'Walrus',
property2: 22
}
};
mockMetadataManagers = {
mockCompObject1: {
values: jasmine.createSpy('metadataManager').andReturn(
Object.values(mockMetadata.mockCompObject1)
)
},
mockCompObject2: {
values: jasmine.createSpy('metadataManager').andReturn(
Object.values(mockMetadata.mockCompObject2)
)
},
mockCompObject3: {
values: jasmine.createSpy('metadataManager').andReturn(
Object.values(mockMetadata.mockCompObject2)
)
}
};
mockComposition = jasmine.createSpyObj('composition', [
'on',
'off',
'load',
'triggerCallback'
]);
mockComposition.on.andCallFake(function (event, callback, context) {
mockEventCallbacks[event] = callback.bind(context);
});
mockComposition.off.andCallFake(function (event) {
unregisterSpies[event]();
});
mockComposition.load.andCallFake(function () {
mockEventCallbacks.add(mockCompObject1);
mockEventCallbacks.add(mockCompObject2);
mockEventCallbacks.load();
});
mockComposition.triggerCallback.andCallFake(function (event) {
if (event === 'add') {
mockEventCallbacks.add(mockCompObject3);
} else if (event === 'remove') {
mockEventCallbacks.remove({
key: 'mockCompObject2'
});
} else {
mockEventCallbacks[event]();
}
});
mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
'request',
'canProvideTelemetry',
'getMetadata',
'subscribe',
'triggerTelemetryCallback'
]);
mockTelemetryAPI.request.andCallFake(function (obj) {
return new Promise(function (resolve, reject) {
resolve(mockTelemetryValues[obj.identifer.key]);
});
});
mockTelemetryAPI.canProvideTelemetry.andReturn(true);
mockTelemetryAPI.getMetadata.andCallFake(function (obj) {
return mockMetadataManagers[obj.identifier.key];
});
mockTelemetryAPI.subscribe.andCallFake(function (obj, callback) {
mockTelemetryCallbacks[obj.identifier.key] = callback;
return unsubscribeSpies[obj.identifier.key];
});
mockTelemetryAPI.triggerTelemetryCallback.andCallFake(function (key) {
mockTelemetryCallbacks[key](mockTelemetryValues2[key]);
});
mockOpenMCT = {
telemetry: mockTelemetryAPI,
composition: {}
};
mockOpenMCT.composition.get = jasmine.createSpy('get').andReturn(mockComposition);
loadCallbackSpy = jasmine.createSpy('loadCallbackSpy');
addCallbackSpy = jasmine.createSpy('addCallbackSpy');
removeCallbackSpy = jasmine.createSpy('removeCallbackSpy');
metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy');
telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy');
conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT);
conditionManager.on('load', loadCallbackSpy);
conditionManager.on('add', addCallbackSpy);
conditionManager.on('remove', removeCallbackSpy);
conditionManager.on('metadata', metadataCallbackSpy);
conditionManager.on('receiveTelemetry', telemetryCallbackSpy);
mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator');
mockConditionEvaluator.execute = jasmine.createSpy('execute');
conditionManager.evaluator = mockConditionEvaluator;
});
it('loads the initial composition and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('load');
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1,
mockCompObject2: mockCompObject2
});
expect(loadCallbackSpy).toHaveBeenCalled();
expect(conditionManager.loadCompleted()).toEqual(true);
});
it('loads metadata from composition and gets it upon request', function () {
expect(conditionManager.getTelemetryMetadata('mockCompObject1'))
.toEqual(mockMetadata.mockCompObject1);
expect(conditionManager.getTelemetryMetadata('mockCompObject2'))
.toEqual(mockMetadata.mockCompObject2);
});
it('maintains lists of global metadata, and does not duplicate repeated fields', function () {
var allKeys = {
property1: {
key: 'property1',
name: 'Property 1'
},
property2: {
key: 'property2',
name: 'Property 2'
},
property3: {
key: 'property3',
name: 'Property 3'
},
property4: {
key: 'property4',
name: 'Property 4'
}
};
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);
mockComposition.triggerCallback('add');
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);
});
it('loads and gets telemetry property types', function () {
conditionManager.parseAllPropertyTypes().then(function () {
expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1'))
.toEqual('string');
expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4'))
.toEqual('number');
expect(conditionManager.metadataLoadComplete()).toEqual(true);
expect(metadataCallbackSpy).toHaveBeenCalled();
});
});
it('responds to a composition add event and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('add');
expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3);
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1,
mockCompObject2: mockCompObject2,
mockCompObject3: mockCompObject3
});
});
it('responds to a composition remove event and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('remove');
expect(removeCallbackSpy).toHaveBeenCalledWith({
key: 'mockCompObject2'
});
expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled();
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1
});
});
it('unregisters telemetry subscriptions and composition listeners on destroy', function () {
mockComposition.triggerCallback('add');
conditionManager.destroy();
Object.values(unsubscribeSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
Object.values(unregisterSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
it('populates its LAD cache with historial data on load, if available', function () {
conditionManager.parseAllPropertyTypes().then(function () {
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a string');
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66);
});
});
it('updates its LAD cache upon recieving telemetry and invokes the appropriate handlers', function () {
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string');
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22);
expect(telemetryCallbackSpy).toHaveBeenCalled();
});
it('evalutes a set of rules and returns the id of the' +
'last active rule, or the first if no rules are active', function () {
var mockRuleOrder = ['default', 'rule0', 'rule1'],
mockRules = {
default: {
getProperty: function () {}
},
rule0: {
getProperty: function () {}
},
rule1: {
getProperty: function () {}
}
};
mockConditionEvaluator.execute.andReturn(false);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default');
mockConditionEvaluator.execute.andReturn(true);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1');
});
it('gets the human-readable name of a composition object', function () {
expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1');
expect(conditionManager.getObjectName('all')).toEqual('all Telemetry');
});
it('gets the human-readable name of a telemetry field', function () {
conditionManager.parseAllPropertyTypes().then(function () {
expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1'))
.toEqual('Property 1');
expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4'))
.toEqual('Property 4');
});
});
it('gets its associated ConditionEvaluator', function () {
expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator);
});
it('allows forcing a receive telemetry event', function () {
conditionManager.triggerTelemetryCallback();
expect(telemetryCallbackSpy).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,157 @@
define(['../src/Condition', 'zepto'], function (Condition, $) {
describe('A summary widget condition', function () {
var testCondition,
mockConfig,
mockConditionManager,
mockContainer,
mockEvaluator,
changeSpy,
duplicateSpy,
removeSpy,
generateValuesSpy;
beforeEach(function () {
mockContainer = $(document.createElement('div'));
mockConfig = {
object: 'object1',
key: 'property1',
operation: 'operation1',
values: [1, 2, 3]
};
mockEvaluator = {};
mockEvaluator.getInputCount = jasmine.createSpy('inputCount');
mockEvaluator.getInputType = jasmine.createSpy('inputType');
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
'on',
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.andReturn(false);
mockConditionManager.metadataLoadCompleted.andReturn(false);
mockConditionManager.getEvaluator.andReturn(mockEvaluator);
mockConditionManager.getComposition.andReturn({});
mockConditionManager.getTelemetryMetadata.andReturn({});
mockConditionManager.getObjectName.andReturn('Object Name');
mockConditionManager.getTelemetryPropertyName.andReturn('Property Name');
duplicateSpy = jasmine.createSpy('duplicate');
removeSpy = jasmine.createSpy('remove');
changeSpy = jasmine.createSpy('change');
generateValuesSpy = jasmine.createSpy('generateValueInputs');
testCondition = new Condition(mockConfig, 54, mockConditionManager);
testCondition.on('duplicate', duplicateSpy);
testCondition.on('remove', removeSpy);
testCondition.on('change', changeSpy);
});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testCondition.getDOM());
expect($('.t-condition', mockContainer).get().length).toEqual(1);
});
it('responds to a change in its object select', function () {
testCondition.selects.object.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'object',
index: 54
});
});
it('responds to a change in its key select', function () {
testCondition.selects.key.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'key',
index: 54
});
});
it('responds to a change in its operation select', function () {
testCondition.generateValueInputs = generateValuesSpy;
testCondition.selects.operation.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'operation',
index: 54
});
expect(generateValuesSpy).toHaveBeenCalledWith('');
});
it('generates value inputs of the appropriate type and quantity', function () {
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.andReturn('number');
mockEvaluator.getInputCount.andReturn(3);
testCondition.generateValueInputs('');
expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(3);
expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(1);
expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(2);
expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(3);
mockEvaluator.getInputType.andReturn('text');
mockEvaluator.getInputCount.andReturn(2);
testCondition.config.values = ['Text I Am', 'Text It Is'];
testCondition.generateValueInputs('');
expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(2);
expect($('input', mockContainer).eq(0).prop('value')).toEqual('Text I Am');
expect($('input', mockContainer).eq(1).prop('value')).toEqual('Text It Is');
});
it('ensures reasonable defaults on values if none are provided', function () {
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.andReturn('number');
mockEvaluator.getInputCount.andReturn(3);
testCondition.config.values = [];
testCondition.generateValueInputs('');
expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(0);
expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(0);
expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(0);
expect(testCondition.config.values).toEqual([0, 0, 0]);
mockEvaluator.getInputType.andReturn('text');
mockEvaluator.getInputCount.andReturn(2);
testCondition.config.values = [];
testCondition.generateValueInputs('');
expect($('input', mockContainer).eq(0).prop('value')).toEqual('');
expect($('input', mockContainer).eq(1).prop('value')).toEqual('');
expect(testCondition.config.values).toEqual(['', '']);
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.andReturn('number');
mockEvaluator.getInputCount.andReturn(3);
testCondition.generateValueInputs('');
$('input', mockContainer).eq(1).prop('value', 9001);
$('input', mockContainer).eq(1).trigger('input');
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'values[1]',
index: 54
});
});
it('can remove itself from the configuration', function () {
testCondition.remove();
expect(removeSpy).toHaveBeenCalledWith(54);
});
it('can duplicate itself', function () {
testCondition.duplicate();
expect(duplicateSpy).toHaveBeenCalledWith({
sourceCondition: mockConfig,
index: 54
});
});
});
});

View File

@ -0,0 +1,269 @@
define(['../src/Rule', 'zepto'], function (Rule, $) {
describe('A Summary Widget Rule', function () {
var mockRuleConfig,
mockDomainObject,
mockOpenMCT,
mockConditionManager,
mockWidgetDnD,
mockEvaluator,
mockContainer,
testRule,
removeSpy,
duplicateSpy,
changeSpy,
conditionChangeSpy;
beforeEach(function () {
mockRuleConfig = {
name: 'Name',
id: 'mockRule',
icon: 'test-icon-name',
style: {
'background-color': '',
'border-color': '',
'color': ''
},
expanded: true,
conditions: [{
object: '',
key: '',
operation: '',
values: []
},{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}]
};
mockDomainObject = {
configuration: {
ruleConfigById: {
mockRule: mockRuleConfig,
otherRule: {}
},
ruleOrder: ['default', 'mockRule', 'otherRule']
}
};
mockOpenMCT = {};
mockOpenMCT.objects = {};
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockEvaluator = {};
mockEvaluator.getOperationDescription = jasmine.createSpy('evaluator')
.andReturn('Operation Description');
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
'on',
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.andReturn(false);
mockConditionManager.metadataLoadCompleted.andReturn(false);
mockConditionManager.getEvaluator.andReturn(mockEvaluator);
mockConditionManager.getComposition.andReturn({});
mockConditionManager.getTelemetryMetadata.andReturn({});
mockConditionManager.getObjectName.andReturn('Object Name');
mockConditionManager.getTelemetryPropertyName.andReturn('Property Name');
mockWidgetDnD = jasmine.createSpyObj('dnd', [
'on',
'setDragImage',
'dragStart'
]);
mockContainer = $(document.createElement('div'));
removeSpy = jasmine.createSpy('removeCallback');
duplicateSpy = jasmine.createSpy('duplicateCallback');
changeSpy = jasmine.createSpy('changeCallback');
conditionChangeSpy = jasmine.createSpy('conditionChangeCallback');
testRule = new Rule(mockRuleConfig, mockDomainObject, mockOpenMCT, mockConditionManager,
mockWidgetDnD);
testRule.on('remove', removeSpy);
testRule.on('duplicate', duplicateSpy);
testRule.on('change', changeSpy);
testRule.on('conditionChange', conditionChangeSpy);
});
it('closes its configuration panel on initial load', function () {
expect(testRule.getProperty('expanded')).toEqual(false);
});
it('gets its DOM element', function () {
mockContainer.append(testRule.getDOM());
expect($('.l-widget-rule', mockContainer).get().length).toBeGreaterThan(0);
});
it('gets its configuration properties', function () {
expect(testRule.getProperty('name')).toEqual('Name');
expect(testRule.getProperty('icon')).toEqual('test-icon-name');
});
it('can duplicate itself', function () {
testRule.duplicate();
mockRuleConfig.expanded = true;
expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig);
});
it('can remove itself from the configuration', function () {
testRule.remove();
expect(removeSpy).toHaveBeenCalled();
expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined();
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']);
});
it('updates its configuration on a condition change and invokes callbacks', function () {
testRule.onConditionChange({
value: 'newValue',
property: 'object',
index: 0
});
expect(testRule.getProperty('conditions')[0].object).toEqual('newValue');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('allows initializing a new condition with a default configuration', function () {
testRule.initCondition();
expect(mockRuleConfig.conditions).toEqual([{
object: '',
key: '',
operation: '',
values: []
},{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
},{
object: '',
key: '',
operation: '',
values: []
}]);
});
it('allows initializing a new condition from a given configuration', function () {
testRule.initCondition({
sourceCondition: {
object: 'object1',
key: 'key1',
operation: 'operation1',
values: [1, 2, 3]
},
index: 0
});
expect(mockRuleConfig.conditions).toEqual([{
object: '',
key: '',
operation: '',
values: []
},{
object: 'object1',
key: 'key1',
operation: 'operation1',
values: [1, 2, 3]
},{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}]);
});
it('invokes mutate when updating the domain object', function () {
testRule.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('builds condition view from condition configuration', function () {
mockContainer.append(testRule.getDOM());
expect($('.t-condition', mockContainer).get().length).toEqual(2);
});
it('responds to input of style properties, and updates the preview', function () {
testRule.colorInputs['background-color'].set('#434343');
expect(mockRuleConfig.style['background-color']).toEqual('#434343');
testRule.colorInputs['border-color'].set('#666666');
expect(mockRuleConfig.style['border-color']).toEqual('#666666');
testRule.colorInputs.color.set('#999999');
expect(mockRuleConfig.style.color).toEqual('#999999');
expect(testRule.thumbnail.css('background-color')).toEqual('rgb(67, 67, 67)');
expect(testRule.thumbnail.css('border-color')).toEqual('rgb(102, 102, 102)');
expect(testRule.thumbnail.css('color')).toEqual('rgb(153, 153, 153)');
expect(changeSpy).toHaveBeenCalled();
});
it('responds to input for the icon property', function () {
testRule.iconInput.set('icon-alert-rect');
expect(mockRuleConfig.icon).toEqual('icon-alert-rect');
expect(changeSpy).toHaveBeenCalled();
});
/*
test for js condition commented out for v1
*/
// it('responds to input of text properties', function () {
// var testInputs = ['name', 'label', 'message', 'jsCondition'],
// input;
// testInputs.forEach(function (key) {
// input = testRule.textInputs[key];
// input.prop('value', 'A new ' + key);
// input.trigger('input');
// expect(mockRuleConfig[key]).toEqual('A new ' + key);
// });
// expect(changeSpy).toHaveBeenCalled();
// });
it('allows input for when the rule triggers', function () {
testRule.trigger.prop('value', 'all');
testRule.trigger.trigger('change');
expect(testRule.config.trigger).toEqual('all');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('generates a human-readable description from its conditions', function () {
testRule.generateDescription();
expect(testRule.config.description).toContain(
'Object Name\'s Property Name Operation Description'
);
testRule.config.trigger = 'js';
testRule.generateDescription();
expect(testRule.config.description).toContain(
'when a custom JavaScript condition evaluates to true'
);
});
it('initiates a drag event when its grippy is clicked', function () {
testRule.grippy.trigger('mousedown');
expect(mockWidgetDnD.setDragImage).toHaveBeenCalled();
expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule');
});
/*
test for js condition commented out for v1
*/
it('can remove a condition from its configuration', function () {
testRule.removeCondition(0);
expect(testRule.config.conditions).toEqual([{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}]);
});
});
});

View File

@ -0,0 +1,167 @@
define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
describe('The Summary Widget', function () {
var summaryWidget,
mockDomainObject,
mockOldDomainObject,
mockOpenMCT,
mockObjectService,
mockStatusCapability,
mockComposition,
mockContainer,
listenCallback,
listenCallbackSpy;
beforeEach(function () {
mockDomainObject = {
identifier: {
key: 'testKey'
},
name: 'testName',
composition: [],
configuration: {}
};
mockComposition = jasmine.createSpyObj('composition', [
'on',
'off',
'load'
]);
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'get',
'listen',
'triggerCallback'
]);
listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {});
mockStatusCapability.get.andReturn([]);
mockStatusCapability.listen.andCallFake(function (callback) {
listenCallback = callback;
return listenCallbackSpy;
});
mockStatusCapability.triggerCallback.andCallFake(function () {
listenCallback(['editing']);
});
mockOldDomainObject = {};
mockOldDomainObject.getCapability = jasmine.createSpy('capability');
mockOldDomainObject.getCapability.andReturn(mockStatusCapability);
mockObjectService = {};
mockObjectService.getObjects = jasmine.createSpy('objectService');
mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) {
resolve({
testKey: mockOldDomainObject
});
}));
mockOpenMCT = jasmine.createSpyObj('openmct', [
'$injector',
'composition',
'objects'
]);
mockOpenMCT.$injector.get = jasmine.createSpy('get');
mockOpenMCT.$injector.get.andReturn(mockObjectService);
mockOpenMCT.composition = jasmine.createSpyObj('composition', [
'get',
'on'
]);
mockOpenMCT.composition.get.andReturn(mockComposition);
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockOpenMCT.objects.observe = jasmine.createSpy('observe');
mockOpenMCT.objects.observe.andReturn(function () {});
summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT);
mockContainer = document.createElement('div');
summaryWidget.show(mockContainer);
});
it('adds its DOM element to the view', function () {
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);
});
it('initialzes a default rule', function () {
expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined();
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']);
});
it('builds rules and rule placeholders in view from configuration', function () {
expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(2);
});
it('allows initializing a new rule with a particular identifier', function () {
summaryWidget.initRule('rule0', 'Rule');
expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined();
});
it('allows adding a new rule with a unique identifier to the configuration and view', function () {
summaryWidget.addRule();
expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2);
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
});
summaryWidget.addRule();
expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3);
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
});
expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(6);
});
it('allows duplicating a rule from source configuration', function () {
var sourceConfig = JSON.parse(JSON.stringify(mockDomainObject.configuration.ruleConfigById.default));
summaryWidget.duplicateRule(sourceConfig);
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2);
});
it('does not duplicate an existing rule in the configuration', function () {
summaryWidget.initRule('default', 'Default');
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1);
});
it('uses mutate when updating the domain object only when in edit mode', function () {
summaryWidget.editing = true;
summaryWidget.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('shows configuration interfaces when in edit mode, and hides them otherwise', function () {
setTimeout(function () {
summaryWidget.onEdit([]);
expect(summaryWidget.editing).toEqual(false);
expect(summaryWidget.ruleArea.css('display')).toEqual('none');
expect(summaryWidget.testDataArea.css('display')).toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).toEqual('none');
summaryWidget.onEdit(['editing']);
expect(summaryWidget.editing).toEqual(true);
expect(summaryWidget.ruleArea.css('display')).not.toEqual('none');
expect(summaryWidget.testDataArea.css('display')).not.toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none');
}, 100);
});
it('unregisters any registered listeners on a destroy', function () {
setTimeout(function () {
summaryWidget.destroy();
expect(listenCallbackSpy).toHaveBeenCalled();
}, 100);
});
it('allows reorders of rules', function () {
summaryWidget.initRule('rule0');
summaryWidget.initRule('rule1');
summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1'];
summaryWidget.reorder({
draggingId: 'rule1',
dropTarget: 'default'
});
expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual(['default', 'rule1', 'rule0']);
});
it('adds hyperlink to the widget button and sets newTab preference', function () {
summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab');
var widgetButton = $('#widget', mockContainer);
expect(widgetButton.attr('href')).toEqual('https://www.nasa.gov');
expect(widgetButton.attr('target')).toEqual('_blank');
});
});
});

View File

@ -0,0 +1,140 @@
define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
describe('A summary widget test data item', function () {
var testDataItem,
mockConfig,
mockConditionManager,
mockContainer,
mockEvaluator,
changeSpy,
duplicateSpy,
removeSpy,
generateValueSpy;
beforeEach(function () {
mockContainer = $(document.createElement('div'));
mockConfig = {
object: 'object1',
key: 'property1',
value: 1
};
mockEvaluator = {};
mockEvaluator.getInputTypeById = jasmine.createSpy('inputType');
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
'on',
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'getTelemetryPropertyType'
]);
mockConditionManager.loadCompleted.andReturn(false);
mockConditionManager.metadataLoadCompleted.andReturn(false);
mockConditionManager.getEvaluator.andReturn(mockEvaluator);
mockConditionManager.getComposition.andReturn({});
mockConditionManager.getTelemetryMetadata.andReturn({});
mockConditionManager.getObjectName.andReturn('Object Name');
mockConditionManager.getTelemetryPropertyName.andReturn('Property Name');
mockConditionManager.getTelemetryPropertyType.andReturn('');
duplicateSpy = jasmine.createSpy('duplicate');
removeSpy = jasmine.createSpy('remove');
changeSpy = jasmine.createSpy('change');
generateValueSpy = jasmine.createSpy('generateValueInput');
testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager);
testDataItem.on('duplicate', duplicateSpy);
testDataItem.on('remove', removeSpy);
testDataItem.on('change', changeSpy);
});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataItem.getDOM());
expect($('.t-test-data-item', mockContainer).get().length).toEqual(1);
});
it('responds to a change in its object select', function () {
testDataItem.selects.object.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'object',
index: 54
});
});
it('responds to a change in its key select', function () {
testDataItem.generateValueInput = generateValueSpy;
testDataItem.selects.key.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'key',
index: 54
});
expect(generateValueSpy).toHaveBeenCalledWith('');
});
it('generates a value input of the appropriate type', function () {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.andReturn('number');
testDataItem.generateValueInput('');
expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1);
expect($('input', mockContainer).prop('valueAsNumber')).toEqual(1);
mockEvaluator.getInputTypeById.andReturn('text');
testDataItem.config.value = 'Text I Am';
testDataItem.generateValueInput('');
expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1);
expect($('input', mockContainer).prop('value')).toEqual('Text I Am');
});
it('ensures reasonable defaults on values if none are provided', function () {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.andReturn('number');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1);
expect($('input', mockContainer).prop('valueAsNumber')).toEqual(0);
expect(testDataItem.config.value).toEqual(0);
mockEvaluator.getInputTypeById.andReturn('text');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1);
expect($('input', mockContainer).prop('value')).toEqual('');
expect(testDataItem.config.value).toEqual('');
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.andReturn('number');
testDataItem.generateValueInput('');
$('input', mockContainer).prop('value', 9001);
$('input', mockContainer).trigger('input');
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'value',
index: 54
});
});
it('can remove itself from the configuration', function () {
testDataItem.remove();
expect(removeSpy).toHaveBeenCalledWith(54);
});
it('can duplicate itself', function () {
testDataItem.duplicate();
expect(duplicateSpy).toHaveBeenCalledWith({
sourceItem: mockConfig,
index: 54
});
});
});
});

View File

@ -0,0 +1,231 @@
define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) {
describe('A Summary Widget Rule', function () {
var mockDomainObject,
mockOpenMCT,
mockConditionManager,
mockEvaluator,
mockContainer,
mockTelemetryMetadata,
testDataManager,
mockCompObject1,
mockCompObject2;
beforeEach(function () {
mockDomainObject = {
configuration: {
testDataConfig: [{
object: '',
key: '',
value: ''
},{
object: 'object1',
key: 'property1',
value: 66
},{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}]
},
composition: [{
object1: {
key: 'object1',
name: 'Object 1'
},
object2: {
key: 'object2',
name: 'Object 2'
}
}]
};
mockTelemetryMetadata = {
object1: {
property1: {
key: 'property1'
},
property2: {
key: 'property2'
}
},
object2 : {
property3: {
key: 'property3'
},
property4: {
key: 'property4'
}
}
};
mockCompObject1 = {
identifier: {
key: 'object1'
},
name: 'Object 1'
};
mockCompObject2 = {
identifier: {
key: 'object2'
},
name: 'Object 2'
};
mockOpenMCT = {};
mockOpenMCT.objects = {};
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockEvaluator = {};
mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache');
mockEvaluator.useTestData = jasmine.createSpy('useTestData');
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
'on',
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'triggerTelemetryCallback'
]);
mockConditionManager.loadCompleted.andReturn(false);
mockConditionManager.metadataLoadCompleted.andReturn(false);
mockConditionManager.getEvaluator.andReturn(mockEvaluator);
mockConditionManager.getComposition.andReturn({
object1: mockCompObject1,
object2: mockCompObject2
});
mockConditionManager.getTelemetryMetadata.andCallFake(function (id) {
return mockTelemetryMetadata[id];
});
mockConditionManager.getObjectName.andReturn('Object Name');
mockConditionManager.getTelemetryPropertyName.andReturn('Property Name');
mockContainer = $(document.createElement('div'));
testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT);
});
it('closes its configuration panel on initial load', function () {
});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataManager.getDOM());
expect($('.t-widget-test-data-content', mockContainer).get().length).toBeGreaterThan(0);
});
it('generates a test cache in the format expected by a condition evaluator', function () {
testDataManager.updateTestCache();
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: {
property1: 66,
property2: ''
},
object2: {
property3: '',
property4: 'Text It Is'
}
});
});
it('updates its configuration on a item change and provides an updated' +
'cache to the evaluator', function () {
testDataManager.onItemChange({
value: 26,
property: 'value',
index: 1
});
expect(testDataManager.config[1].value).toEqual(26);
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: {
property1: 26,
property2: ''
},
object2: {
property3: '',
property4: 'Text It Is'
}
});
});
it('allows initializing a new item with a default configuration', function () {
testDataManager.initItem();
expect(mockDomainObject.configuration.testDataConfig).toEqual([{
object: '',
key: '',
value: ''
},{
object: 'object1',
key: 'property1',
value: 66
},{
object: 'object2',
key: 'property4',
value: 'Text It Is'
},{
object: '',
key: '',
value: ''
}]);
});
it('allows initializing a new item from a given configuration', function () {
testDataManager.initItem({
sourceItem: {
object: 'object2',
key: 'property3',
value: 1
},
index: 0
});
expect(mockDomainObject.configuration.testDataConfig).toEqual([{
object: '',
key: '',
value: ''
},{
object: 'object2',
key: 'property3',
value: 1
},{
object: 'object1',
key: 'property1',
value: 66
},{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}]);
});
it('invokes mutate when updating the domain object', function () {
testDataManager.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('builds item view from item configuration', function () {
mockContainer.append(testDataManager.getDOM());
expect($('.t-test-data-item', mockContainer).get().length).toEqual(3);
});
it('can remove a item from its configuration', function () {
testDataManager.removeItem(0);
expect(mockDomainObject.configuration.testDataConfig).toEqual([{
object: 'object1',
key: 'property1',
value: 66
},{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}]);
});
it('exposes a UI element to toggle test data on and off', function () {
});
});
});

View File

@ -0,0 +1,23 @@
define(['../../src/input/ColorPalette'], function (ColorPalette) {
describe('An Open MCT color palette', function () {
var colorPalette, changeCallback;
beforeEach(function () {
changeCallback = jasmine.createSpy('changeCallback');
});
it('allows defining a custom color set', function () {
colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']);
expect(colorPalette.getCurrent()).toEqual('color1');
colorPalette.on('change', changeCallback);
colorPalette.set('color2');
expect(colorPalette.getCurrent()).toEqual('color2');
expect(changeCallback).toHaveBeenCalledWith('color2');
});
it('loads with a default color set if one is not provided', function () {
colorPalette = new ColorPalette('someClass', 'someContainer');
expect(colorPalette.getCurrent()).toBeDefined();
});
});
});

View File

@ -0,0 +1,23 @@
define(['../../src/input/IconPalette'], function (IconPalette) {
describe('An Open MCT icon palette', function () {
var iconPalette, changeCallback;
beforeEach(function () {
changeCallback = jasmine.createSpy('changeCallback');
});
it('allows defining a custom icon set', function () {
iconPalette = new IconPalette('','someContainer', ['icon1', 'icon2', 'icon3']);
expect(iconPalette.getCurrent()).toEqual('icon1');
iconPalette.on('change', changeCallback);
iconPalette.set('icon2');
expect(iconPalette.getCurrent()).toEqual('icon2');
expect(changeCallback).toHaveBeenCalledWith('icon2');
});
it('loads with a default icon set if one is not provided', function () {
iconPalette = new IconPalette('someClass', 'someContainer');
expect(iconPalette.getCurrent()).toBeDefined();
});
});
});

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