Merge remote-tracking branch 'origin/open181' into rems_data_merge

This commit is contained in:
Andrew Henry
2015-10-22 21:31:45 -07:00
67 changed files with 3865 additions and 2097 deletions

View File

@ -103,6 +103,21 @@ This build will:
Run as `mvn clean install`. Run as `mvn clean install`.
### Building Documentation
Open MCT Web's documentation is generated by an
[npm](https://www.npmjs.com/)-based build:
* `npm install` _(only needs to run once)_
* `npm run docs`
Documentation will be generated in `target/docs`. Note that diagram
generation is dependent on having [Cairo](http://cairographics.org/download/)
installed; see
[node-canvas](https://github.com/Automattic/node-canvas#installation)'s
documentation for help with installation.
# Glossary # Glossary
Certain terms are used throughout Open MCT Web with consistent meanings Certain terms are used throughout Open MCT Web with consistent meanings

View File

@ -34,6 +34,10 @@
{ {
"key": "time", "key": "time",
"name": "Time" "name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
} }
], ],
"ranges": [ "ranges": [
@ -61,4 +65,4 @@
} }
] ]
} }
} }

View File

@ -29,23 +29,25 @@ define(
function () { function () {
"use strict"; "use strict";
var firstObservedTime = Math.floor(Date.now() / 1000); var ONE_DAY = 60 * 60 * 24,
firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY;
/** /**
* *
* @constructor * @constructor
*/ */
function SinewaveTelemetrySeries(request) { function SinewaveTelemetrySeries(request) {
var latestObservedTime = Math.floor(Date.now() / 1000), var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0,
latestTime = Math.floor(Date.now() / 1000) - timeOffset,
firstTime = firstObservedTime - timeOffset,
endTime = (request.end !== undefined) ? endTime = (request.end !== undefined) ?
Math.floor(request.end / 1000) : latestObservedTime, Math.floor(request.end / 1000) : latestTime,
count = count = Math.min(endTime, latestTime) - firstTime,
Math.min(endTime, latestObservedTime) - firstObservedTime, period = +request.period || 30,
period = request.period || 30,
generatorData = {}, generatorData = {},
offset = (request.start !== undefined) ? requestStart = (request.start === undefined) ? firstTime :
Math.floor(request.start / 1000) - firstObservedTime : Math.max(Math.floor(request.start / 1000), firstTime),
0; offset = requestStart - firstTime;
if (request.size !== undefined) { if (request.size !== undefined) {
offset = Math.max(offset, count - request.size); offset = Math.max(offset, count - request.size);
@ -56,8 +58,8 @@ define(
}; };
generatorData.getDomainValue = function (i, domain) { generatorData.getDomainValue = function (i, domain) {
return (i + offset) * 1000 + return (i + offset) * 1000 + firstTime * 1000 -
(domain !== 'delta' ? (firstObservedTime * 1000) : 0); (domain === 'yesterday' ? ONE_DAY : 0);
}; };
generatorData.getRangeValue = function (i, range) { generatorData.getRangeValue = function (i, range) {

View File

@ -4,7 +4,11 @@
{ {
"implementation": "WatchIndicator.js", "implementation": "WatchIndicator.js",
"depends": ["$interval", "$rootScope"] "depends": ["$interval", "$rootScope"]
},
{
"implementation": "DigestIndicator.js",
"depends": ["$interval", "$rootScope"]
} }
] ]
} }
} }

View File

@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* Displays the number of digests that have occurred since the
* indicator was first instantiated.
* @constructor
* @param $interval Angular's $interval
* @implements {Indicator}
*/
function DigestIndicator($interval, $rootScope) {
var digests = 0,
displayed = 0,
start = Date.now();
function update() {
var secs = (Date.now() - start) / 1000;
displayed = Math.round(digests / secs);
}
function increment() {
digests += 1;
}
$rootScope.$watch(increment);
// Update state every second
$interval(update, 1000);
// Provide initial state, too
update();
return {
getGlyph: function () {
return ".";
},
getGlyphClass: function () {
return undefined;
},
getText: function () {
return displayed + " digests/sec";
},
getDescription: function () {
return "";
}
};
}
return DigestIndicator;
}
);

View File

@ -42,9 +42,8 @@
</div> </div>
</div> </div>
<div class='object-holder abs vscroll'> <mct-representation key="representation.selected.key"
<mct-representation key="representation.selected.key" mct-object="representation.selected.key && domainObject"
mct-object="representation.selected.key && domainObject"> class="abs object-holder">
</mct-representation> </mct-representation>
</div>
</span> </span>

View File

@ -28,7 +28,9 @@
<mct-split-pane class='contents abs' anchor='left'> <mct-split-pane class='contents abs' anchor='left'>
<div class='split-pane-component treeview pane left'> <div class='split-pane-component treeview pane left'>
<div class="holder abs l-mobile"> <div class="holder abs l-mobile">
<mct-representation key="'create-button'" mct-object="navigatedObject"> <mct-representation key="'create-button'"
mct-object="navigatedObject"
mct-device="desktop">
</mct-representation> </mct-representation>
<div class='holder search-holder abs' <div class='holder search-holder abs'
ng-class="{active: treeModel.search}"> ng-class="{active: treeModel.search}">

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="menu-element wrapper" ng-controller="ClickAwayController as createController"> <div class="menu-element wrapper" ng-controller="ClickAwayController as createController">
<div class="s-menu major create-btn" ng-click="createController.toggle()"> <div class="s-menu-btn major create-btn" ng-click="createController.toggle()">
<span class="ui-symbol icon type-icon">&#x2b;</span> <span class="ui-symbol icon type-icon">&#x2b;</span>
<span class="title-label">Create</span> <span class="title-label">Create</span>
</div> </div>

View File

@ -30,12 +30,11 @@
structure="toolbar.structure" structure="toolbar.structure"
ng-model="toolbar.state"> ng-model="toolbar.state">
</mct-toolbar> </mct-toolbar>
<div class='holder abs object-holder work-area'> <mct-representation key="representation.selected.key"
<mct-representation key="representation.selected.key" toolbar="toolbar"
toolbar="toolbar" mct-object="representation.selected.key && domainObject"
mct-object="representation.selected.key && domainObject"> class="holder abs object-holder work-area">
</mct-representation> </mct-representation>
</div>
</div> </div>
<mct-splitter></mct-splitter> <mct-splitter></mct-splitter>
<div <div

View File

@ -45,6 +45,7 @@ $ueEditToolBarH: 25px;
$ueBrowseLeftPaneW: 25%; $ueBrowseLeftPaneW: 25%;
$ueEditLeftPaneW: 75%; $ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px; $treeSearchInputBarH: 25px;
$ueTimeControlH: (33px, 20px, 20px);
// Overlay // Overlay
$ovrTopBarH: 60px; $ovrTopBarH: 60px;
$ovrFooterH: 30px; $ovrFooterH: 30px;

View File

@ -40,11 +40,11 @@
/************************** HTML ENTITIES */ /************************** HTML ENTITIES */
a { a {
color: #ccc; color: $colorA;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
color: #fff; color: $colorAHov;
} }
} }
@ -125,6 +125,14 @@ mct-container {
text-align: center; text-align: center;
} }
.scrolling {
overflow: auto;
}
.vscroll {
overflow-y: auto;
}
.no-margin { .no-margin {
margin: 0; margin: 0;
} }

View File

@ -29,6 +29,9 @@
} }
.ui-symbol { .ui-symbol {
&.type-icon {
color: $colorObjHdrIc;
}
&.icon { &.icon {
color: $colorKey; color: $colorKey;
&.alert { &.alert {
@ -41,6 +44,9 @@
font-size: 1.65em; font-size: 1.65em;
} }
} }
&.icon-calendar:after {
content: "\e605";
}
} }
.bar .ui-symbol { .bar .ui-symbol {
@ -52,7 +58,7 @@
display: inline-block; display: inline-block;
} }
.s-menu .invoke-menu, .s-menu-btn .invoke-menu,
.icon.major .invoke-menu { .icon.major .invoke-menu {
margin-left: $interiorMarginSm; margin-left: $interiorMarginSm;
} }

View File

@ -347,9 +347,10 @@
/* This doesn't work on an element inside an element with absolute positioning that has height: auto */ /* This doesn't work on an element inside an element with absolute positioning that has height: auto */
//position: relative; //position: relative;
top: 50%; top: 50%;
-webkit-transform: translateY(-50%); @include webkitProp(transform, translateY(-50%));
-ms-transform: translateY(-50%); //-webkit-transform: translateY(-50%);
transform: translateY(-50%); //-ms-transform: translateY(-50%);
//transform: translateY(-50%);
} }
@mixin verticalCenterBlock($holderH, $itemH) { @mixin verticalCenterBlock($holderH, $itemH) {
@ -374,22 +375,8 @@
overflow-y: $showBar; overflow-y: $showBar;
} }
@mixin wait-spinner($b: 5px, $c: $colorAlt1) {
display: block;
position: absolute;
-webkit-animation: rotation .6s infinite linear;
-moz-animation: rotation .6s infinite linear;
-o-animation: rotation .6s infinite linear;
animation: rotation .6s infinite linear;
border-color: rgba($c, 0.25);
border-top-color: rgba($c, 1.0);
border-style: solid;
border-width: $b;
border-radius: 100%;
}
@mixin test($c: #ffcc00, $a: 0.2) { @mixin test($c: #ffcc00, $a: 0.2) {
background-color: rgba($c, $a); background-color: rgba($c, $a) !important;
} }
@mixin tmpBorder($c: #ffcc00, $a: 0.75) { @mixin tmpBorder($c: #ffcc00, $a: 0.75) {

View File

@ -10,9 +10,6 @@
&.fixed { &.fixed {
font-size: 0.8em; font-size: 0.8em;
} }
&.scrolling {
overflow: auto;
}
.controls, .controls,
label, label,
.inline-block { .inline-block {

View File

@ -205,7 +205,7 @@ label.checkbox.custom {
} }
} }
.s-menu label.checkbox.custom { .s-menu-btn label.checkbox.custom {
margin-left: 5px; margin-left: 5px;
} }
@ -295,49 +295,155 @@ label.checkbox.custom {
.slider { .slider {
$knobH: 100%; //14px; $knobH: 100%; //14px;
$knobW: 12px;
$slotH: 50%;
.slot { .slot {
// @include border-radius($basicCr * .75); // @include border-radius($basicCr * .75);
@include sliderTrack(); //@include sliderTrack();
height: $slotH;
width: auto; width: auto;
position: absolute; position: absolute;
top: ($knobH - $slotH) / 2; top: 0;
right: 0; right: 0;
bottom: auto; bottom: 0;
left: 0; left: 0;
} }
.knob { .knob {
@include btnSubtle(); //@include btnSubtle();
@include controlGrippy(rgba(black, 0.3), vertical, 1px, solid); //@include controlGrippy(rgba(black, 0.3), vertical, 1px, solid);
cursor: ew-resize; @include trans-prop-nice-fade(.25s);
background-color: $sliderColorKnob;
&:hover {
background-color: $sliderColorKnobHov;
}
position: absolute; position: absolute;
height: $knobH; height: $knobH;
width: $knobW; width: $sliderKnobW;
top: 0; top: 0;
auto: 0; auto: 0;
bottom: auto; bottom: auto;
left: auto; left: auto;
&:before { }
top: 1px; .knob-l {
bottom: 3px; @include border-left-radius($sliderKnobW);
left: ($knobW / 2) - 1; cursor: w-resize;
} }
.knob-r {
@include border-right-radius($sliderKnobW);
cursor: e-resize;
} }
.range { .range {
background: rgba($colorKey, 0.6); @include trans-prop-nice-fade(.25s);
background-color: $sliderColorRange;
cursor: ew-resize; cursor: ew-resize;
position: absolute; position: absolute;
top: 0; top: 0; //$tbOffset;
right: auto; right: auto;
bottom: 0; bottom: 0;
left: auto; left: auto;
height: auto; height: auto;
width: auto; width: auto;
&:hover { &:hover {
background: rgba($colorKey, 0.7); background-color: $sliderColorRangeHov;
}
}
}
/******************************************************** DATETIME PICKER */
.l-datetime-picker {
$r1H: 15px;
@include user-select(none);
font-size: 0.8rem;
padding: $interiorMarginLg !important;
width: 230px;
.l-month-year-pager {
$pagerW: 20px;
//@include test();
//font-size: 0.8rem;
height: $r1H;
margin-bottom: $interiorMargin;
position: relative;
.pager,
.val {
//@include test(red);
@extend .abs;
}
.pager {
width: $pagerW;
@extend .ui-symbol;
&.prev {
right: auto;
&:before {
content: "\3c";
}
}
&.next {
left: auto;
text-align: right;
&:before {
content: "\3e";
}
}
}
.val {
text-align: center;
left: $pagerW + $interiorMargin;
right: $pagerW + $interiorMargin;
}
}
.l-calendar,
.l-time-selects {
border-top: 1px solid $colorInteriorBorder
}
.l-time-selects {
line-height: $formInputH;
}
}
/******************************************************** CALENDAR */
.l-calendar {
$colorMuted: pushBack($colorMenuFg, 30%);
ul.l-cal-row {
@include display-flex;
@include flex-flow(row nowrap);
margin-top: 1px;
&:first-child {
margin-top: 0;
}
li {
@include flex(1 0);
//@include test();
margin-left: 1px;
padding: $interiorMargin;
text-align: center;
&:first-child {
margin-left: 0;
}
}
&.l-header li {
color: $colorMuted;
}
&.l-body li {
@include trans-prop-nice(background-color, .25s);
cursor: pointer;
&.in-month {
background-color: $colorCalCellInMonthBg;
}
.sub {
color: $colorMuted;
font-size: 0.8em;
}
&.selected {
background: $colorCalCellSelectedBg;
color: $colorCalCellSelectedFg;
.sub {
color: inherit;
}
}
&:hover {
background-color: $colorCalCellHovBg;
color: $colorCalCellHovFg;
.sub {
color: inherit;
}
}
} }
} }
} }

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/******************************************************** MENU BUTTONS */ /******************************************************** MENU BUTTONS */
.s-menu { .s-menu-btn {
// Formerly .btn-menu // Formerly .btn-menu
@extend .s-btn; @extend .s-btn;
span.l-click-area { span.l-click-area {
@ -62,186 +62,192 @@
/******************************************************** MENUS THEMSELVES */ /******************************************************** MENUS THEMSELVES */
.menu-element { .menu-element {
$bg: $colorMenuBg;
$fg: $colorMenuFg;
$ic: $colorMenuIc;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
.menu { }
@include border-radius($basicCr);
@include containerSubtle($bg, $fg);
@include boxShdw($shdwMenu);
@include txtShdw($shdwMenuText);
display: block; // set to block via jQuery
padding: $interiorMarginSm 0;
position: absolute;
z-index: 10;
ul {
@include menuUlReset();
li {
@include box-sizing(border-box);
border-top: 1px solid lighten($bg, 20%);
color: pullForward($bg, 60%);
line-height: $menuLineH;
padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
position: relative;
white-space: nowrap;
&:first-child {
border: none;
}
&:hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
.icon {
color: $colorMenuHovIc;
}
}
.type-icon {
left: $interiorMargin * 2;
}
}
}
}
.menu, .s-menu {
.context-menu, @include border-radius($basicCr);
.super-menu { @include containerSubtle($colorMenuBg, $colorMenuFg);
pointer-events: auto; @include boxShdw($shdwMenu);
ul li { @include txtShdw($shdwMenuText);
//padding-left: 25px; padding: $interiorMarginSm 0;
a { }
color: $fg;
}
.icon {
color: $ic;
}
.type-icon {
left: $interiorMargin;
}
&:hover .icon {
//color: lighten($ic, 5%);
}
}
}
.checkbox-menu { .menu {
// Used in search dropdown in tree @extend .s-menu;
@extend .context-menu; display: block;
ul li { position: absolute;
padding-left: 50px; z-index: 10;
.checkbox { ul {
$d: 0.7rem; @include menuUlReset();
position: absolute; li {
left: $interiorMargin;
top: ($menuLineH - $d) / 1.5;
em {
height: $d;
width: $d;
&:before {
font-size: 7px !important;// $d/2;
height: $d;
width: $d;
line-height: $d;
}
}
}
.type-icon {
left: 25px;
}
}
}
.super-menu {
$w: 500px;
$h: $w - 20;
$plw: 50%;
$prw: 50%;
display: block;
width: $w;
height: $h;
.contents {
@include absPosDefault($interiorMargin);
}
.pane {
@include box-sizing(border-box); @include box-sizing(border-box);
&.left { border-top: 1px solid lighten($colorMenuBg, 20%);
//@include test(); color: pullForward($colorMenuBg, 60%);
border-right: 1px solid pullForward($colorMenuBg, 10%); line-height: $menuLineH;
left: 0; padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
padding-right: $interiorMargin; position: relative;
right: auto; white-space: nowrap;
width: $plw; &:first-child {
overflow-x: hidden; border: none;
overflow-y: auto; }
ul { &:hover {
li { background: $colorMenuHovBg;
@include border-radius($controlCr); color: $colorMenuHovFg;
padding-left: 30px; .icon {
border-top: none; color: $colorMenuHovIc;
}
} }
} }
&.right { .type-icon {
//@include test(red); left: $interiorMargin * 2;
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
} }
} }
.menu-item-description {
.desc-area {
&.icon {
$h: 150px;
color: $colorCreateMenuLgIcon;
position: relative;
font-size: 8em;
left: 0;
height: $h;
line-height: $h;
margin-bottom: $interiorMargin * 5;
text-align: center;
}
&.title {
color: $colorCreateMenuText;
font-size: 1.2em;
margin-bottom: 0.5em;
}
&.description {
//color: lighten($bg, 30%);
color: $colorCreateMenuText;
font-size: 0.8em;
line-height: 1.5em;
}
}
}
}
.context-menu {
font-size: 0.80rem;
} }
} }
.context-menu-holder { .menu,
pointer-events: none; .context-menu,
.super-menu {
pointer-events: auto;
ul li {
//padding-left: 25px;
a {
color: $colorMenuFg;
}
.icon {
color: $colorMenuIc;
}
.type-icon {
left: $interiorMargin;
}
&:hover .icon {
//color: lighten($colorMenuIc, 5%);
}
}
}
.checkbox-menu {
// Used in search dropdown in tree
@extend .context-menu;
ul li {
padding-left: 50px;
.checkbox {
$d: 0.7rem;
position: absolute;
left: $interiorMargin;
top: ($menuLineH - $d) / 1.5;
em {
height: $d;
width: $d;
&:before {
font-size: 7px !important;// $d/2;
height: $d;
width: $d;
line-height: $d;
}
}
}
.type-icon {
left: 25px;
}
}
}
.super-menu {
$w: 500px;
$h: $w - 20;
$plw: 50%;
$prw: 50%;
display: block;
width: $w;
height: $h;
.contents {
@include absPosDefault($interiorMargin);
}
.pane {
@include box-sizing(border-box);
&.left {
//@include test();
border-right: 1px solid pullForward($colorMenuBg, 10%);
left: 0;
padding-right: $interiorMargin;
right: auto;
width: $plw;
overflow-x: hidden;
overflow-y: auto;
ul {
li {
@include border-radius($controlCr);
padding-left: 30px;
border-top: none;
}
}
}
&.right {
//@include test(red);
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
}
}
.menu-item-description {
.desc-area {
&.icon {
$h: 150px;
color: $colorCreateMenuLgIcon;
position: relative;
font-size: 8em;
left: 0;
height: $h;
line-height: $h;
margin-bottom: $interiorMargin * 5;
text-align: center;
}
&.title {
color: $colorCreateMenuText;
font-size: 1.2em;
margin-bottom: 0.5em;
}
&.description {
//color: lighten($colorMenuBg, 30%);
color: $colorCreateMenuText;
font-size: 0.8em;
line-height: 1.5em;
}
}
}
}
.context-menu {
font-size: 0.80rem;
}
.context-menu-holder,
.menu-holder {
position: absolute; position: absolute;
height: 200px;
width: 170px;
z-index: 70; z-index: 70;
.context-menu-wrapper { .context-menu-wrapper {
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
.context-menu {
}
} }
&.go-left .context-menu { &.go-left .context-menu,
&.go-left .menu {
right: 0; right: 0;
} }
&.go-up .context-menu { &.go-up .context-menu,
&.go-up .menu {
bottom: 0; bottom: 0;
} }
} }
.context-menu-holder {
pointer-events: none;
height: 200px;
width: 170px;
}
.btn-bar.right .menu, .btn-bar.right .menu,
.menus-to-left .menu { .menus-to-left .menu {
left: auto; left: auto;

View File

@ -1,72 +1,155 @@
.l-time-controller { @mixin toiLineHovEffects() {
$inputTxtW: 90px; //@include pulse(.25s);
$knobW: 9px; &:before,
$r1H: 20px; &:after {
$r2H: 30px; background-color: $timeControllerToiLineColorHov;
$r3H: 10px; }
}
position: relative; .l-time-controller-visible {
margin: $interiorMarginLg 0;
min-width: 400px; }
mct-include.l-time-controller {
$minW: 500px;
$knobHOffset: 0px;
$knobM: ($sliderKnobW + $knobHOffset) * -1;
$rangeValPad: $interiorMargin;
$rangeValOffset: $sliderKnobW;
//$knobCr: $sliderKnobW;
$timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
$r1H: nth($ueTimeControlH,1);
$r2H: nth($ueTimeControlH,2);
$r3H: nth($ueTimeControlH,3);
@include absPosDefault();
//@include test();
display: block;
top: auto;
height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
min-width: $minW;
font-size: 0.8rem;
.l-time-range-inputs-holder, .l-time-range-inputs-holder,
.l-time-range-slider { .l-time-range-slider {
font-size: 0.8em; //font-size: 0.8em;
} }
.l-time-range-inputs-holder, .l-time-range-inputs-holder,
.l-time-range-slider-holder, .l-time-range-slider-holder,
.l-time-range-ticks-holder .l-time-range-ticks-holder
{ {
margin-bottom: $interiorMargin; //@include test();
position: relative; @include absPosDefault(0, visible);
@include box-sizing(border-box);
top: auto;
} }
.l-time-range-slider, .l-time-range-slider,
.l-time-range-ticks { .l-time-range-ticks {
//@include test(red, 0.1); //@include test(red, 0.1);
@include absPosDefault(0, visible); @include absPosDefault(0, visible);
left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset;
} }
.l-time-range-inputs-holder { .l-time-range-inputs-holder {
height: $r1H; //@include test(red);
} height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
padding-top: $interiorMargin;
.l-time-range-slider, border-top: 1px solid $colorInteriorBorder;
.l-time-range-ticks { .type-icon {
left: $inputTxtW; right: $inputTxtW; font-size: 120%;
vertical-align: middle;
}
.l-time-range-input,
.l-time-range-inputs-elem {
margin-right: $interiorMargin;
.lbl {
color: $colorPlotLabelFg;
}
.ui-symbol.icon {
font-size: 11px;
width: 11px;
}
}
} }
.l-time-range-slider-holder { .l-time-range-slider-holder {
height: $r2H; //@include test(green);
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
.range-holder { .range-holder {
@include box-shadow(none); @include box-shadow(none);
background: none; background: none;
border: none; border: none;
height: 75%; .range {
.toi-line {
$myC: $timeControllerToiLineColor;
$myW: 8px;
@include transform(translateX(50%));
position: absolute;
//@include test();
top: 0; right: 0; bottom: 0px; left: auto;
width: $myW;
height: auto;
z-index: 2;
&:before,
&:after {
background-color: $myC;
content: "";
position: absolute;
}
&:before {
// Vert line
top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
width: 2px;
//top: 0; right: 3px; bottom: 0; left: 3px;
}
&:after {
// Circle element
@include border-radius($myW);
@include transform(translateY(-50%));
top: 50%; right: 0; bottom: auto; left: 0;
width: auto;
height: $myW;
}
}
&:hover .toi-line {
@include toiLineHovEffects;
}
}
}
&:not(:active) {
//@include test(#ff00cc);
.knob,
.range {
@include transition-property(left, right);
@include transition-duration(500ms);
@include transition-timing-function(ease-in-out);
}
} }
} }
.l-time-range-ticks-holder { .l-time-range-ticks-holder {
height: $r3H; height: $r3H;
.l-time-range-ticks { .l-time-range-ticks {
border-top: 1px solid $colorInteriorBorder; border-top: 1px solid $colorTick;
.tick { .tick {
background-color: $colorInteriorBorder; background-color: $colorTick;
border:none; border:none;
height: 5px;
width: 1px; width: 1px;
margin-left: -1px; margin-left: -1px;
position: absolute;
&:first-child { &:first-child {
margin-left: 0; margin-left: 0;
} }
.l-time-range-tick-label { .l-time-range-tick-label {
color: lighten($colorInteriorBorder, 20%); @include webkitProp(transform, translateX(-50%));
font-size: 0.7em; color: $colorPlotLabelFg;
display: inline-block;
font-size: 0.9em;
position: absolute; position: absolute;
margin-left: -0.5 * $tickLblW; top: 8px;
text-align: center; white-space: nowrap;
top: $r3H;
width: $tickLblW;
z-index: 2; z-index: 2;
} }
} }
@ -74,31 +157,47 @@
} }
.knob { .knob {
width: $knobW; z-index: 2;
.range-value { .range-value {
$w: 75px; //@include test($sliderColorRange);
//@include test(); @include trans-prop-nice-fade(.25s);
padding: 0 $rangeValOffset;
position: absolute; position: absolute;
top: 50%; height: $r2H;
margin-top: -7px; // Label is 13px high line-height: $r2H;
white-space: nowrap; white-space: nowrap;
width: $w;
} }
&:hover .range-value { &:hover .range-value {
color: $colorKey; color: $sliderColorKnobHov;
} }
&.knob-l { &.knob-l {
margin-left: $knobW / -2; //@include border-bottom-left-radius($knobCr); // MOVED TO _CONTROLS.SCSS
margin-left: $knobM;
.range-value { .range-value {
text-align: right; text-align: right;
right: $knobW + $interiorMargin; right: $rangeValOffset;
} }
} }
&.knob-r { &.knob-r {
margin-right: $knobW / -2; //@include border-bottom-right-radius($knobCr);
margin-right: $knobM;
.range-value { .range-value {
left: $knobW + $interiorMargin; left: $rangeValOffset;
}
&:hover + .range-holder .range .toi-line {
@include toiLineHovEffects;
} }
} }
} }
}
//.slot.range-holder {
// background-color: $sliderColorRangeHolder;
//}
.s-time-range-val {
//@include test();
@include border-radius($controlCr);
background-color: $colorInputBg;
padding: 1px 1px 0 $interiorMargin;
} }

View File

@ -19,39 +19,44 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
@mixin complexFieldHolder($myW) {
width: $myW + $interiorMargin;
input[type="text"] {
width: $myW;
}
}
.complex.datetime { .complex.datetime {
span { span {
display: inline-block; display: inline-block;
margin-right: $interiorMargin; margin-right: $interiorMargin;
} }
/*
.field-hints, .field-hints,
.fields { .fields {
} }
.field-hints { .field-hints {
} }
*/
.fields { .fields {
margin-top: $interiorMarginSm 0; margin-top: $interiorMarginSm 0;
padding: $interiorMarginSm 0; padding: $interiorMarginSm 0;
} }
.date { .date {
$myW: 80px; @include complexFieldHolder(80px);
width: $myW + $interiorMargin; }
input {
width: $myW; .time.md {
} @include complexFieldHolder(60px);
} }
.time.sm { .time.sm {
$myW: 40px; @include complexFieldHolder(40px);
width: $myW + $interiorMargin;
input {
width: $myW;
}
} }
} }

View File

@ -21,10 +21,13 @@
*****************************************************************************/ *****************************************************************************/
.select { .select {
@include btnSubtle($colorSelectBg); @include btnSubtle($colorSelectBg);
margin: 0 0 2px 2px; // Needed to avoid dropshadow from being clipped by parent containers @if $shdwBtns != none {
margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers
}
padding: 0 $interiorMargin; padding: 0 $interiorMargin;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
line-height: $formInputH;
select { select {
@include appearance(none); @include appearance(none);
@include box-sizing(border-box); @include box-sizing(border-box);
@ -40,11 +43,8 @@
} }
&:after { &:after {
@include contextArrow(); @include contextArrow();
pointer-events: none;
color: rgba($colorSelectFg, percentToDecimal($contrastInvokeMenuPercent)); color: rgba($colorSelectFg, percentToDecimal($contrastInvokeMenuPercent));
//content:"v";
//display: block;
//font-family: 'symbolsfont';
//pointer-events: none;
position: absolute; position: absolute;
right: $interiorMargin; top: 0; right: $interiorMargin; top: 0;
} }

View File

@ -19,24 +19,45 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
@-webkit-keyframes rotation { @include keyframes(rotation) {
from {-webkit-transform: rotate(0deg);} 0% { transform: rotate(0deg); }
to {-webkit-transform: rotate(359deg);} 100% { transform: rotate(359deg); }
} }
@-moz-keyframes rotation { @mixin wait-spinner2($b: 5px, $c: $colorAlt1) {
from {-moz-transform: rotate(0deg);} @include keyframes(rotateCentered) {
to {-moz-transform: rotate(359deg);} 0% { transform: translateX(-50%) translateY(-50%) rotate(0deg); }
100% { transform: translateX(-50%) translateY(-50%) rotate(359deg); }
}
@include animation-name(rotateCentered);
@include animation-duration(0.5s);
@include animation-iteration-count(infinite);
@include animation-timing-function(linear);
border-color: rgba($c, 0.25);
border-top-color: rgba($c, 1.0);
border-style: solid;
border-width: 5px;
@include border-radius(100%);
@include box-sizing(border-box);
display: block;
position: absolute;
height: 0; width: 0;
padding: 7%;
left: 50%; top: 50%;
} }
@-o-keyframes rotation { @mixin wait-spinner($b: 5px, $c: $colorAlt1) {
from {-o-transform: rotate(0deg);} display: block;
to {-o-transform: rotate(359deg);} position: absolute;
} -webkit-animation: rotation .6s infinite linear;
-moz-animation: rotation .6s infinite linear;
@keyframes rotation { -o-animation: rotation .6s infinite linear;
from {transform: rotate(0deg);} animation: rotation .6s infinite linear;
to {transform: rotate(359deg);} border-color: rgba($c, 0.25);
border-top-color: rgba($c, 1.0);
border-style: solid;
border-width: $b;
@include border-radius(100%);
} }
.t-wait-spinner, .t-wait-spinner,
@ -96,4 +117,28 @@
margin-top: 0 !important; margin-top: 0 !important;
padding: 0 !important; padding: 0 !important;
top: 0; left: 0; top: 0; left: 0;
}
.loading {
// Can be applied to any block element with height and width
pointer-events: none;
&:before,
&:after {
content: '';
}
&:before {
@include wait-spinner2(5px, $colorLoadingFg);
z-index: 10;
}
&:after {
@include absPosDefault();
background: $colorLoadingBg;
display: block;
z-index: 9;
}
&.tree-item:before {
padding: $menuLineH / 4;
border-width: 2px;
}
} }

View File

@ -40,6 +40,11 @@ table {
thead, .thead { thead, .thead {
border-bottom: 1px solid $colorTabHeaderBorder; border-bottom: 1px solid $colorTabHeaderBorder;
} }
&:not(.fixed-header) tr th {
background-color: $colorTabHeaderBg;
}
tbody, .tbody { tbody, .tbody {
display: table-row-group; display: table-row-group;
tr, .tr { tr, .tr {
@ -64,7 +69,6 @@ table {
display: table-cell; display: table-cell;
} }
th, .th { th, .th {
background-color: $colorTabHeaderBg;
border-left: 1px solid $colorTabHeaderBorder; border-left: 1px solid $colorTabHeaderBorder;
color: $colorTabHeaderFg; color: $colorTabHeaderFg;
padding: $tabularTdPadLR $tabularTdPadLR; padding: $tabularTdPadLR $tabularTdPadLR;
@ -143,7 +147,7 @@ table {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: $tabularHeaderH; height: $tabularHeaderH;
background: rgba(#fff, 0.15); background-color: $colorTabHeaderBg;
} }
} }
tbody, .tbody { tbody, .tbody {

View File

@ -89,7 +89,7 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa
.gl-plot-label, .gl-plot-label,
.l-plot-label { .l-plot-label {
// @include test(yellow); // @include test(yellow);
color: lighten($colorBodyFg, 20%); color: $colorPlotLabelFg;
position: absolute; position: absolute;
text-align: center; text-align: center;
// text-transform: uppercase; // text-transform: uppercase;

View File

@ -214,8 +214,6 @@
.search-scroll { .search-scroll {
order: 3; order: 3;
//padding-right: $rightPadding;
margin-top: 4px; margin-top: 4px;
// Adjustable scrolling size // Adjustable scrolling size
@ -227,28 +225,6 @@
.load-icon { .load-icon {
position: relative; position: relative;
&.loading {
pointer-events: none;
margin-left: $leftMargin;
.title-label {
// Text styling
font-style: italic;
font-size: .9em;
opacity: 0.5;
// Text positioning
margin-left: $iconWidth + $leftMargin;
line-height: 24px;
}
.wait-spinner {
margin-left: $leftMargin;
}
}
&:not(.loading) {
cursor: pointer;
}
} }
.load-more-button { .load-more-button {

View File

@ -83,7 +83,6 @@ ul.tree {
.icon { .icon {
&.l-icon-link, &.l-icon-link,
&.l-icon-alert { &.l-icon-alert {
//@include txtShdw($shdwItemTreeIcon);
position: absolute; position: absolute;
z-index: 2; z-index: 2;
} }
@ -105,26 +104,12 @@ ul.tree {
@include absPosDefault(); @include absPosDefault();
display: block; display: block;
left: $runningItemW + ($interiorMargin * 3); left: $runningItemW + ($interiorMargin * 3);
//right: $treeContextTriggerW + $interiorMargin;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
&.loading {
pointer-events: none;
.label {
opacity: 0.5;
.title-label {
font-style: italic;
}
}
.wait-spinner {
margin-left: 14px;
}
}
&.selected { &.selected {
background: $colorItemTreeSelectedBg; background: $colorItemTreeSelectedBg;
color: $colorItemTreeSelectedFg; color: $colorItemTreeSelectedFg;
@ -142,9 +127,6 @@ ul.tree {
&:hover { &:hover {
background: rgba($colorBodyFg, 0.1); //lighten($colorBodyBg, 5%); background: rgba($colorBodyFg, 0.1); //lighten($colorBodyBg, 5%);
color: pullForward($colorBodyFg, 20%); color: pullForward($colorBodyFg, 20%);
//.context-trigger {
// display: block;
//}
.icon { .icon {
color: $colorItemTreeIconHover; color: $colorItemTreeIconHover;
} }
@ -158,7 +140,6 @@ ul.tree {
.context-trigger { .context-trigger {
$h: 0.9rem; $h: 0.9rem;
//display: none;
top: -1px; top: -1px;
position: absolute; position: absolute;
right: $interiorMarginSm; right: $interiorMarginSm;

View File

@ -47,7 +47,7 @@
} }
&.frame-template { &.frame-template {
.s-btn, .s-btn,
.s-menu { .s-menu-btn {
height: $ohH; height: $ohH;
line-height: $ohH; line-height: $ohH;
padding: 0 $interiorMargin; padding: 0 $interiorMargin;
@ -56,7 +56,7 @@
} }
} }
.s-menu:after { .s-menu-btn:after {
font-size: 8px; font-size: 8px;
} }

View File

@ -30,21 +30,21 @@
} }
.holder-all { .holder-all {
$myM: 0; // $interiorMarginSm; $myM: 0; // $interiorMarginSm;
top: $myM; top: $myM;
right: $myM; right: $myM;
bottom: $myM; bottom: $myM;
left: $myM; left: $myM;
} }
.browse-area, .browse-area,
.edit-area, .edit-area,
.editor { .editor {
position: absolute; position: absolute;
} }
.editor { .editor {
@include border-radius($basicCr * 1.5); @include border-radius($basicCr * 1.5);
} }
.contents { .contents {
@ -68,8 +68,8 @@
margin-right: $interiorMargin; margin-right: $interiorMargin;
} }
&.abs { &.abs {
text-wrap: none; text-wrap: none;
white-space: nowrap; white-space: nowrap;
&.left, &.left,
.left { .left {
width: 45%; width: 45%;
@ -95,51 +95,51 @@
} }
.user-environ { .user-environ {
.browse-area, .browse-area,
.edit-area, .edit-area,
.editor { .editor {
top: $bodyMargin + $ueTopBarH + ($interiorMargin); top: $bodyMargin + $ueTopBarH + ($interiorMargin);
right: $bodyMargin; right: $bodyMargin;
bottom: $ueFooterH + $bodyMargin; bottom: $ueFooterH + $bodyMargin;
left: $bodyMargin; left: $bodyMargin;
} }
.browse-area, .browse-area,
.edit-area { .edit-area {
> .contents { > .contents {
left: 0; left: 0;
right: 0; right: 0;
} }
} }
.edit-area { .edit-area {
$tbH: $btnToolbarH + $interiorMargin; $tbH: $btnToolbarH + $interiorMargin;
top: $bodyMargin + $ueTopBarEditH + ($interiorMargin); top: $bodyMargin + $ueTopBarEditH + ($interiorMargin);
.tool-bar { .tool-bar {
bottom: auto; bottom: auto;
height: $tbH; height: $tbH;
line-height: $btnToolbarH; line-height: $btnToolbarH;
} }
.work-area { .work-area {
top: $tbH + $interiorMargin * 2; top: $tbH + $interiorMargin * 2;
} }
} }
.ue-bottom-bar { .ue-bottom-bar {
//@include absPosDefault($bodyMargin); //@include absPosDefault($bodyMargin);
@include absPosDefault(0);// New status bar design @include absPosDefault(0); // New status bar design
top: auto; top: auto;
height: $ueFooterH; height: $ueFooterH;
.status-holder { .status-holder {
//right: $ueAppLogoW + $bodyMargin; New status bar design //right: $ueAppLogoW + $bodyMargin; New status bar design
z-index: 1; z-index: 1;
} }
.app-logo { .app-logo {
left: auto; left: auto;
width: $ueAppLogoW; width: $ueAppLogoW;
z-index: 2; z-index: 2;
} }
} }
} }
.cols { .cols {
@ -225,89 +225,89 @@
} }
} }
.pane { .pane {
position: absolute; position: absolute;
&.treeview.left { &.treeview.left {
.create-btn-holder { .create-btn-holder {
bottom: auto; top: 0; bottom: auto;
height: $ueTopBarH; top: 0;
.wrapper.menu-element { height: $ueTopBarH;
position: absolute; .wrapper.menu-element {
bottom: $interiorMargin; position: absolute;
} bottom: $interiorMargin;
} }
.search-holder { }
top: $ueTopBarH + $interiorMarginLg; .search-holder {
} top: $ueTopBarH + $interiorMarginLg;
.tree-holder { }
overflow: auto; .tree-holder {
top: $ueTopBarH + $interiorMarginLg + $treeSearchInputBarH + $interiorMargin; overflow: auto;
} top: $ueTopBarH + $interiorMarginLg + $treeSearchInputBarH + $interiorMargin;
}
} }
&.items { &.items {
.object-browse-bar { .object-browse-bar {
.left.abs, .left.abs,
.right.abs { .right.abs {
top: auto; top: auto;
} }
//.left.abs {
// @include tmpBorder(green);
//}
//.right.abs {
// @include tmpBorder(red);
//}
}
.object-holder {
top: $ueTopBarH + $interiorMarginLg;
} }
} }
.object-holder {
overflow: auto;
}
} }
.split-layout { .split-layout {
&.horizontal { &.horizontal {
// Slides up and down // Slides up and down
>.pane { > .pane {
// @include test(); // @include test();
margin-top: $interiorMargin; margin-top: $interiorMargin;
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
} }
} }
} }
&.vertical { &.vertical {
// Slides left and right // Slides left and right
>.pane { > .pane {
// @include test(); // @include test();
margin-left: $interiorMargin; margin-left: $interiorMargin;
>.holder { > .holder {
left: 0; left: 0;
right: 0; right: 0;
} }
&:first-child { &:first-child {
margin-left: 0; margin-left: 0;
.holder { .holder {
right: $interiorMarginSm; right: $interiorMarginSm;
} }
} }
} }
} }
}
.object-holder {
overflow: hidden; // Contained objects need to handle their own overflow now
top: $ueTopBarH + $interiorMarginLg;
> ng-include {
@include absPosDefault(0, auto);
}
&.l-controls-visible {
&.l-time-controller-visible {
bottom: nth($ueTimeControlH,1) + nth($ueTimeControlH,2) +nth($ueTimeControlH,3) + ($interiorMargin * 3);
}
}
} }
.object-browse-bar .s-btn, .object-browse-bar .s-btn,
.top-bar .buttons-main .s-btn, .top-bar .buttons-main .s-btn,
.top-bar .s-menu, .top-bar .s-menu-btn,
.tool-bar .s-btn, .tool-bar .s-btn,
.tool-bar .s-menu { .tool-bar .s-menu-btn {
$h: $btnToolbarH; $h: $btnToolbarH;
height: $h; height: $h;
line-height: $h; line-height: $h;
vertical-align: top; vertical-align: top;
} }
.object-browse-bar, .object-browse-bar,
@ -318,33 +318,29 @@
} }
.object-browse-bar { .object-browse-bar {
//@include test(blue); //@include test(blue);
@include absPosDefault(0, visible); @include absPosDefault(0, visible);
@include box-sizing(border-box); @include box-sizing(border-box);
height: $ueTopBarH; height: $ueTopBarH;
line-height: $ueTopBarH; line-height: $ueTopBarH;
white-space: nowrap; white-space: nowrap;
.left { .left {
padding-right: $interiorMarginLg * 2; padding-right: $interiorMarginLg * 2;
.l-back { .l-back {
display: inline-block; display: inline-block;
float: left; float: left;
margin-right: $interiorMarginLg; margin-right: $interiorMarginLg;
} }
} }
} }
.l-flex { .l-flex {
@include webkitVal('display', 'flex'); @include webkitVal('display', 'flex');
@include webkitProp('flex-flow', 'row nowrap'); @include webkitProp('flex-flow', 'row nowrap');
.left { .left {
//@include test(red); //@include test(red);
@include webkitProp(flex, '1 1 0'); @include webkitProp(flex, '1 1 0');
padding-right: $interiorMarginLg; padding-right: $interiorMarginLg;
} }
} }
.vscroll {
overflow-y: auto;
}

View File

@ -20,42 +20,47 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-controller="DateTimePickerController"> <div ng-controller="DateTimePickerController" class="l-datetime-picker s-datetime-picker s-menu">
<div style="vertical-align: top; display: inline-block"> <div class="holder">
<div style="text-align: center;"> <div class="l-month-year-pager">
<a ng-click="changeMonth(-1)">&lt;</a> <a class="pager prev" ng-click="changeMonth(-1)"></a>
{{month}} {{year}} <span class="val">{{month}} {{year}}</span>
<a ng-click="changeMonth(1)">&gt;</a> <a class="pager next" ng-click="changeMonth(1)"></a>
</div> </div>
<div> <div class="l-calendar">
<table> <ul class="l-cal-row l-header">
<tr> <li ng-repeat="day in ['Su','Mo','Tu','We','Th','Fr','Sa']">{{day}}</li>
<th ng-repeat="day in ['Su','Mo','Tu','We','Th','Fr','Sa']"> </ul>
{{day}} <ul class="l-cal-row l-body" ng-repeat="row in table">
</th> <li ng-repeat="cell in row"
</tr> ng-click="select(cell)"
<tr ng-repeat="row in table"> ng-class='{ "in-month": isInCurrentMonth(cell), selected: isSelected(cell) }'>
<td style="text-align: center;" <div class="prime">{{cell.day}}</div>
ng-repeat="cell in row" <div class="sub">{{cell.dayOfYear}}</div>
ng-click="select(cell)" </li>
ng-class='{ </ul>
disabled: !isSelectable(cell), </div>
test: isSelected(cell) </div>
}'> <div class="l-time-selects complex datetime"
<div>{{cell.day}}</div> ng-show="options">
<div style="font-size: 80%">{{cell.dayOfYear}}</div> <div class="field-hints">
</td> <span class="hint time md"
</tr> ng-repeat="key in ['hours', 'minutes', 'seconds']"
</table> ng-if="options[key]">
</div> {{nameFor(key)}}
</div> </span>
<div style="vertical-align: top; display: inline-block" </div>
ng-repeat="key in ['hours', 'minutes', 'seconds']" <div>
ng-if="options[key]"> <span class="field control time md"
<div>{{nameFor(key)}}</div> ng-repeat="key in ['hours', 'minutes', 'seconds']"
<select size="10" ng-if="options[key]">
ng-model="time[key]" <div class='form-control select'>
ng-options="i for i in optionsFor(key)"> <select size="1"
</select> ng-model="time[key]"
</div> ng-options="i for i in optionsFor(key)">
</select>
</div>
</span>
</div>
</div>
</div> </div>

View File

@ -21,7 +21,7 @@
--> -->
<span ng-controller="ViewSwitcherController"> <span ng-controller="ViewSwitcherController">
<div <div
class="view-switcher menu-element s-menu" class="view-switcher menu-element s-menu-btn"
ng-if="view.length > 1" ng-if="view.length > 1"
ng-controller="ClickAwayController as toggle" ng-controller="ClickAwayController as toggle"
> >

View File

@ -19,53 +19,57 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-controller="TimeRangeController">
<div class="l-time-controller" ng-controller="TimeRangeController">
<div class="l-time-range-inputs-holder"> <div class="l-time-range-inputs-holder">
Start: {{startOuterText}} <span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span ng-controller="ToggleController as t"> <span class="l-time-range-input" ng-controller="ToggleController as t1">
<a class="ui-symbol" ng-click="t.toggle()">p</a> <!--<span class="lbl">Start</span>-->
<mct-popup ng-if="t.isActive()"> <span class="s-btn time-range-start">
<div style="background: #222;" <input type="text"
mct-click-elsewhere="t.setState(false)"> ng-model="boundsModel.start"
<mct-control key="'datetime-picker'" ng-class="{ error: !boundsModel.startValid }">
ng-model="ngModel.outer" </input>
field="'start'" <a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
options="{ hours: true }"> <mct-popup ng-if="t1.isActive()">
</mct-control> <div mct-click-elsewhere="t1.setState(false)">
</div> <mct-control key="'datetime-picker'"
</mct-popup> ng-model="ngModel.outer"
field="'start'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>
</span> </span>
End: {{endOuterText}} <span class="l-time-range-inputs-elem lbl">to</span>
<span ng-controller="ToggleController as t2">
<a class="ui-symbol" ng-click="t2.toggle()">p</a> <span class="l-time-range-input" ng-controller="ToggleController as t2">
<mct-popup ng-if="t2.isActive()"> <!--<span class="lbl">End</span>-->
<div style="background: #222;" <span class="s-btn l-time-range-input">
mct-click-elsewhere="t2.setState(false)"> <input type="text"
<mct-control key="'datetime-picker'" ng-model="boundsModel.end"
ng-model="ngModel.outer" ng-class="{ error: !boundsModel.endValid }">
field="'end'" </input>
options="{ hours: true }"> <a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
</mct-control> </a>
</div> <mct-popup ng-if="t2.isActive()">
</mct-popup> <div mct-click-elsewhere="t2.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'end'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>&nbsp;
</span> </span>
</div> </div>
<div class="l-time-range-slider-holder"> <div class="l-time-range-slider-holder">
<div class="l-time-range-slider"> <div class="l-time-range-slider">
<div class="slider" <div class="slider"
mct-resize="spanWidth = bounds.width"> mct-resize="spanWidth = bounds.width">
<div class="slot range-holder">
<div class="range"
mct-drag-down="startMiddleDrag()"
mct-drag="middleDrag(delta[0])"
ng-style="{ left: startInnerPct, right: endInnerPct}">
</div>
</div>
<div class="knob knob-l" <div class="knob knob-l"
mct-drag-down="startLeftDrag()" mct-drag-down="startLeftDrag()"
mct-drag="leftDrag(delta[0])" mct-drag="leftDrag(delta[0])"
@ -78,6 +82,14 @@
ng-style="{ right: endInnerPct }"> ng-style="{ right: endInnerPct }">
<div class="range-value">{{endInnerText}}</div> <div class="range-value">{{endInnerText}}</div>
</div> </div>
<div class="slot range-holder">
<div class="range"
mct-drag-down="startMiddleDrag()"
mct-drag="middleDrag(delta[0])"
ng-style="{ left: startInnerPct, right: endInnerPct}">
<div class="toi-line"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -138,7 +138,7 @@ define(
$scope.ngModel[$scope.field] = m.valueOf(); $scope.ngModel[$scope.field] = m.valueOf();
} }
$scope.isSelectable = function (cell) { $scope.isInCurrentMonth = function (cell) {
return cell.month === month; return cell.month === month;
}; };

View File

@ -26,7 +26,8 @@ define(
function (moment) { function (moment) {
"use strict"; "use strict";
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss"; var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
TICK_SPACING_PX = 150;
/** /**
* @memberof platform/commonUI/general * @memberof platform/commonUI/general
@ -34,12 +35,23 @@ define(
*/ */
function TimeConductorController($scope, now) { function TimeConductorController($scope, now) {
var tickCount = 2, var tickCount = 2,
innerMinimumSpan = 1000, // 1 second
outerMinimumSpan = 1000 * 60 * 60, // 1 hour
initialDragValue; initialDragValue;
function formatTimestamp(ts) { function formatTimestamp(ts) {
return moment.utc(ts).format(DATE_FORMAT); return moment.utc(ts).format(DATE_FORMAT);
} }
function parseTimestamp(text) {
var m = moment.utc(text, DATE_FORMAT);
if (m.isValid()) {
return m.valueOf();
} else {
throw new Error("Could not parse " + text);
}
}
// From 0.0-1.0 to "0%"-"1%" // From 0.0-1.0 to "0%"-"1%"
function toPercent(p) { function toPercent(p) {
return (100 * p) + "%"; return (100 * p) + "%";
@ -59,8 +71,7 @@ define(
} }
function updateSpanWidth(w) { function updateSpanWidth(w) {
// Space about 100px apart tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
tickCount = Math.max(Math.floor(w / 100), 2);
updateTicks(); updateTicks();
} }
@ -90,6 +101,25 @@ define(
return { start: bounds.start, end: bounds.end }; return { start: bounds.start, end: bounds.end };
} }
function updateBoundsTextForProperty(ngModel, property) {
try {
if (!$scope.boundsModel[property] ||
parseTimestamp($scope.boundsModel[property]) !==
ngModel.outer[property]) {
$scope.boundsModel[property] =
formatTimestamp(ngModel.outer[property]);
}
} catch (e) {
// User-entered text is invalid, so leave it be
// until they fix it.
}
}
function updateBoundsText(ngModel) {
updateBoundsTextForProperty(ngModel, 'start');
updateBoundsTextForProperty(ngModel, 'end');
}
function updateViewFromModel(ngModel) { function updateViewFromModel(ngModel) {
var t = now(); var t = now();
@ -98,8 +128,7 @@ define(
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer); ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
// First, dates for the date pickers for outer bounds // First, dates for the date pickers for outer bounds
$scope.startOuterDate = new Date(ngModel.outer.start); updateBoundsText(ngModel);
$scope.endOuterDate = new Date(ngModel.outer.end);
// Then various updates for the inner span // Then various updates for the inner span
updateViewForInnerSpanFromModel(ngModel); updateViewForInnerSpanFromModel(ngModel);
@ -139,7 +168,7 @@ define(
$scope.ngModel.inner.start = clamp( $scope.ngModel.inner.start = clamp(
initialDragValue + delta, initialDragValue + delta,
$scope.ngModel.outer.start, $scope.ngModel.outer.start,
$scope.ngModel.inner.end $scope.ngModel.inner.end - innerMinimumSpan
); );
updateViewFromModel($scope.ngModel); updateViewFromModel($scope.ngModel);
} }
@ -148,7 +177,7 @@ define(
var delta = toMillis(pixels); var delta = toMillis(pixels);
$scope.ngModel.inner.end = clamp( $scope.ngModel.inner.end = clamp(
initialDragValue + delta, initialDragValue + delta,
$scope.ngModel.inner.start, $scope.ngModel.inner.start + innerMinimumSpan,
$scope.ngModel.outer.end $scope.ngModel.outer.end
); );
updateViewFromModel($scope.ngModel); updateViewFromModel($scope.ngModel);
@ -174,30 +203,76 @@ define(
function updateOuterStart(t) { function updateOuterStart(t) {
var ngModel = $scope.ngModel; var ngModel = $scope.ngModel;
ngModel.outer.end =
Math.max(ngModel.outer.start, ngModel.outer.end); ngModel.outer.start = t;
ngModel.outer.end = Math.max(
ngModel.outer.start + outerMinimumSpan,
ngModel.outer.end
);
ngModel.inner.start = ngModel.inner.start =
Math.max(ngModel.outer.start, ngModel.inner.start); Math.max(ngModel.outer.start, ngModel.inner.start);
ngModel.inner.end = ngModel.inner.end = Math.max(
Math.max(ngModel.outer.start, ngModel.inner.end); ngModel.inner.start + innerMinimumSpan,
ngModel.inner.end
$scope.startOuterText = formatTimestamp(t); );
updateViewForInnerSpanFromModel(ngModel); updateViewForInnerSpanFromModel(ngModel);
updateTicks();
} }
function updateOuterEnd(t) { function updateOuterEnd(t) {
var ngModel = $scope.ngModel; var ngModel = $scope.ngModel;
ngModel.outer.start =
Math.min(ngModel.outer.end, ngModel.outer.start); ngModel.outer.end = t;
ngModel.inner.start =
Math.min(ngModel.outer.end, ngModel.inner.start); ngModel.outer.start = Math.min(
ngModel.outer.end - outerMinimumSpan,
ngModel.outer.start
);
ngModel.inner.end = ngModel.inner.end =
Math.min(ngModel.outer.end, ngModel.inner.end); Math.min(ngModel.outer.end, ngModel.inner.end);
ngModel.inner.start = Math.min(
$scope.endOuterText = formatTimestamp(t); ngModel.inner.end - innerMinimumSpan,
ngModel.inner.start
);
updateViewForInnerSpanFromModel(ngModel); updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
function updateStartFromText(value) {
try {
updateOuterStart(parseTimestamp(value));
updateBoundsTextForProperty($scope.ngModel, 'end');
$scope.boundsModel.startValid = true;
} catch (e) {
$scope.boundsModel.startValid = false;
return;
}
}
function updateEndFromText(value) {
try {
updateOuterEnd(parseTimestamp(value));
updateBoundsTextForProperty($scope.ngModel, 'start');
$scope.boundsModel.endValid = true;
} catch (e) {
$scope.boundsModel.endValid = false;
return;
}
}
function updateStartFromPicker(value) {
updateOuterStart(value);
updateBoundsText($scope.ngModel);
}
function updateEndFromPicker(value) {
updateOuterEnd(value);
updateBoundsText($scope.ngModel);
} }
$scope.startLeftDrag = startLeftDrag; $scope.startLeftDrag = startLeftDrag;
@ -209,14 +284,17 @@ define(
$scope.state = false; $scope.state = false;
$scope.ticks = []; $scope.ticks = [];
$scope.boundsModel = {};
// Initialize scope to defaults // Initialize scope to defaults
updateViewFromModel($scope.ngModel); updateViewFromModel($scope.ngModel);
$scope.$watchCollection("ngModel", updateViewFromModel); $scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth); $scope.$watch("spanWidth", updateSpanWidth);
$scope.$watch("ngModel.outer.start", updateOuterStart); $scope.$watch("ngModel.outer.start", updateStartFromPicker);
$scope.$watch("ngModel.outer.end", updateOuterEnd); $scope.$watch("ngModel.outer.end", updateEndFromPicker);
$scope.$watch("boundsModel.start", updateStartFromText);
$scope.$watch("boundsModel.end", updateEndFromText);
} }
return TimeConductorController; return TimeConductorController;

View File

@ -22,10 +22,15 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define( define(
["../../src/controllers/TimeRangeController"], ["../../src/controllers/TimeRangeController", "moment"],
function (TimeRangeController) { function (TimeRangeController, moment) {
"use strict"; "use strict";
var SEC = 1000,
MIN = 60 * SEC,
HOUR = 60 * MIN,
DAY = 24 * HOUR;
describe("The TimeRangeController", function () { describe("The TimeRangeController", function () {
var mockScope, var mockScope,
mockNow, mockNow,
@ -61,6 +66,171 @@ define(
.toHaveBeenCalledWith("ngModel", jasmine.any(Function)); .toHaveBeenCalledWith("ngModel", jasmine.any(Function));
}); });
describe("when dragged", function () {
beforeEach(function () {
mockScope.ngModel = {
outer: {
start: DAY * 1000,
end: DAY * 1001
},
inner: {
start: DAY * 1000 + HOUR * 3,
end: DAY * 1001 - HOUR * 3
}
};
mockScope.spanWidth = 1000;
fireWatch("spanWidth", mockScope.spanWidth);
fireWatchCollection("ngModel", mockScope.ngModel);
});
it("updates the start time for left drags", function () {
mockScope.startLeftDrag();
mockScope.leftDrag(250);
expect(mockScope.ngModel.inner.start)
.toEqual(DAY * 1000 + HOUR * 9);
});
it("updates the end time for right drags", function () {
mockScope.startRightDrag();
mockScope.rightDrag(-250);
expect(mockScope.ngModel.inner.end)
.toEqual(DAY * 1000 + HOUR * 15);
});
it("updates both start and end for middle drags", function () {
mockScope.startMiddleDrag();
mockScope.middleDrag(-125);
expect(mockScope.ngModel.inner).toEqual({
start: DAY * 1000,
end: DAY * 1000 + HOUR * 18
});
mockScope.middleDrag(250);
expect(mockScope.ngModel.inner).toEqual({
start: DAY * 1000 + HOUR * 6,
end: DAY * 1001
});
});
it("enforces a minimum inner span", function () {
mockScope.startRightDrag();
mockScope.rightDrag(-9999999);
expect(mockScope.ngModel.inner.end)
.toBeGreaterThan(mockScope.ngModel.inner.start);
});
});
describe("when outer bounds are changed", function () {
beforeEach(function () {
mockScope.ngModel = {
outer: {
start: DAY * 1000,
end: DAY * 1001
},
inner: {
start: DAY * 1000 + HOUR * 3,
end: DAY * 1001 - HOUR * 3
}
};
mockScope.spanWidth = 1000;
fireWatch("spanWidth", mockScope.spanWidth);
fireWatchCollection("ngModel", mockScope.ngModel);
});
it("enforces a minimum outer span", function () {
mockScope.ngModel.outer.end =
mockScope.ngModel.outer.start - DAY * 100;
fireWatch(
"ngModel.outer.end",
mockScope.ngModel.outer.end
);
expect(mockScope.ngModel.outer.end)
.toBeGreaterThan(mockScope.ngModel.outer.start);
mockScope.ngModel.outer.start =
mockScope.ngModel.outer.end + DAY * 100;
fireWatch(
"ngModel.outer.start",
mockScope.ngModel.outer.start
);
expect(mockScope.ngModel.outer.end)
.toBeGreaterThan(mockScope.ngModel.outer.start);
});
it("enforces a minimum inner span when outer span changes", function () {
mockScope.ngModel.outer.end =
mockScope.ngModel.outer.start - DAY * 100;
fireWatch(
"ngModel.outer.end",
mockScope.ngModel.outer.end
);
expect(mockScope.ngModel.inner.end)
.toBeGreaterThan(mockScope.ngModel.inner.start);
});
describe("by typing", function () {
it("updates models", function () {
var newStart = "1977-05-25 17:30:00",
newEnd = "2015-12-18 03:30:00";
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.ngModel.outer.start)
.toEqual(moment.utc(newStart).valueOf());
expect(mockScope.boundsModel.startValid)
.toBeTruthy();
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.ngModel.outer.end)
.toEqual(moment.utc(newEnd).valueOf());
expect(mockScope.boundsModel.endValid)
.toBeTruthy();
});
it("displays error state", function () {
var newStart = "Not a date",
newEnd = "Definitely not a date",
oldStart = mockScope.ngModel.outer.start,
oldEnd = mockScope.ngModel.outer.end;
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.ngModel.outer.start)
.toEqual(oldStart);
expect(mockScope.boundsModel.startValid)
.toBeFalsy();
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.ngModel.outer.end)
.toEqual(oldEnd);
expect(mockScope.boundsModel.endValid)
.toBeFalsy();
});
it("does not modify user input", function () {
// Don't want the controller "fixing" bad or
// irregularly-formatted input out from under
// the user's fingertips.
var newStart = "Not a date",
newEnd = "2015-3-3 01:02:04",
oldStart = mockScope.ngModel.outer.start,
oldEnd = mockScope.ngModel.outer.end;
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.boundsModel.start)
.toEqual(newStart);
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.boundsModel.end)
.toEqual(newEnd);
});
});
});
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,14 @@ $colorKey: #0099cc;
$colorKeySelectedBg: #005177; $colorKeySelectedBg: #005177;
$colorKeyFg: #fff; $colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.1); $colorInteriorBorder: rgba($colorBodyFg, 0.1);
$colorA: #ccc;
$colorAHov: #fff;
$contrastRatioPercent: 7%; $contrastRatioPercent: 7%;
$basicCr: 2px; $basicCr: 2px;
$controlCr: 3px; $controlCr: 3px;
$smallCr: 2px; $smallCr: 2px;
// Buttons // Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent); // $colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent); //
$colorBtnFg: $colorBodyFg; $colorBtnFg: $colorBodyFg;
$colorBtnMajorBg: $colorKey; $colorBtnMajorBg: $colorKey;
@ -20,6 +22,18 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: $colorKey; $colorBtnIcon: $colorKey;
$colorInvokeMenu: #fff; $colorInvokeMenu: #fff;
$contrastInvokeMenuPercent: 20%; $contrastInvokeMenuPercent: 20%;
$shdwBtns: rgba(black, 0.2) 0 1px 2px;
$sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.1);
$sliderColorRange: rgba($sliderColorBase, 0.3);
$sliderColorRangeHov: rgba($sliderColorBase, 0.5);
$sliderColorKnob: rgba($sliderColorBase, 0.6);
$sliderColorKnobHov: $sliderColorBase;
$sliderColorRangeValHovBg: rgba($sliderColorBase, 0.1);
$sliderColorRangeValHovFg: $colorKeyFg;
$sliderKnobW: nth($ueTimeControlH,2)/2;
$timeControllerToiLineColor: #00c2ff;
$timeControllerToiLineColorHov: #fff;
// General Colors // General Colors
$colorAlt1: #ffc700; $colorAlt1: #ffc700;
@ -34,6 +48,7 @@ $colorFormSectionHeader: rgba(#000, 0.2);
$colorInvokeMenu: #fff; $colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg; $colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pullForward($colorObjHdrTxt, 20%); $colorObjHdrIc: pullForward($colorObjHdrTxt, 20%);
$colorTick: rgba(white, 0.2);
// Menu colors // Menu colors
$colorMenuBg: pullForward($colorBodyBg, 23%); $colorMenuBg: pullForward($colorBodyBg, 23%);
@ -99,16 +114,17 @@ $colorItemBgSelected: $colorKey;
$colorTabBorder: pullForward($colorBodyBg, 10%); $colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: darken($colorBodyBg, 10%); $colorTabBodyBg: darken($colorBodyBg, 10%);
$colorTabBodyFg: lighten($colorTabBodyBg, 40%); $colorTabBodyFg: lighten($colorTabBodyBg, 40%);
$colorTabHeaderBg: lighten($colorBodyBg, 10%); $colorTabHeaderBg: rgba(white, 0.1); // lighten($colorBodyBg, 10%);
$colorTabHeaderFg: lighten($colorTabHeaderBg, 40%); $colorTabHeaderFg: $colorBodyFg; //lighten($colorTabHeaderBg, 40%);
$colorTabHeaderBorder: $colorBodyBg; $colorTabHeaderBorder: $colorBodyBg;
// Plot // Plot
$colorPlotBg: rgba(black, 0.1); $colorPlotBg: rgba(black, 0.1);
$colorPlotFg: $colorBodyFg; $colorPlotFg: $colorBodyFg;
$colorPlotHash: rgba(white, 0.2); $colorPlotHash: $colorTick;
$stylePlotHash: dashed; $stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder; $colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree // Tree
$colorItemTreeIcon: $colorKey; $colorItemTreeIcon: $colorKey;
@ -139,5 +155,16 @@ $colorGrippyInteriorHover: $colorKey;
// Mobile // Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 5%); $colorMobilePaneLeft: darken($colorBodyBg, 5%);
// Datetime Picker
$colorCalCellHovBg: $colorKey;
$colorCalCellHovFg: $colorKeyFg;
$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
// About Screen // About Screen
$colorAboutLink: #84b3ff; $colorAboutLink: #84b3ff;
// Loading
$colorLoadingBg: rgba($colorBodyFg, 0.2);
$colorLoadingFg: $colorAlt1;

View File

@ -1,13 +1,13 @@
@mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg, $hover: false) { @mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg, $hover: false) {
@include containerBase($bg, $fg); @include containerBase($bg, $fg);
@include background-image(linear-gradient(lighten($bg, 5%), $bg)); @include background-image(linear-gradient(lighten($bg, 5%), $bg));
@include boxShdwSubtle(); @include boxShdw($shdwBtns);
} }
@mixin btnSubtle($bg: $colorBodyBg, $bgHov: none, $fg: $colorBodyFg, $ic: $colorBtnIcon) { @mixin btnSubtle($bg: $colorBodyBg, $bgHov: none, $fg: $colorBodyFg, $ic: $colorBtnIcon) {
@include containerSubtle($bg, $fg); @include containerSubtle($bg, $fg);
@include btnBase($bg, linear-gradient(lighten($bg, 15%), lighten($bg, 10%)), $fg, $ic); @include btnBase($bg, linear-gradient(lighten($bg, 15%), lighten($bg, 10%)), $fg, $ic);
@include text-shadow(rgba(black, 0.3) 0 1px 1px); @include text-shadow($shdwItemText);
} }
@function pullForward($c: $colorBodyBg, $p: 20%) { @function pullForward($c: $colorBodyBg, $p: 20%) {

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,14 @@ $colorKey: #0099cc;
$colorKeySelectedBg: $colorKey; $colorKeySelectedBg: $colorKey;
$colorKeyFg: #fff; $colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.2); $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #999;
$colorAHov: $colorKey;
$contrastRatioPercent: 40%; $contrastRatioPercent: 40%;
$basicCr: 4px; $basicCr: 4px;
$controlCr: $basicCr; $controlCr: $basicCr;
$smallCr: 3px; $smallCr: 3px;
// Buttons // Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent); $colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent);
$colorBtnFg: #fff; $colorBtnFg: #fff;
$colorBtnMajorBg: $colorKey; $colorBtnMajorBg: $colorKey;
@ -20,9 +22,21 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: #eee; $colorBtnIcon: #eee;
$colorInvokeMenu: #000; $colorInvokeMenu: #000;
$contrastInvokeMenuPercent: 40%; $contrastInvokeMenuPercent: 40%;
$shdwBtns: none;
$sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.07);
$sliderColorRange: rgba($sliderColorBase, 0.2);
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
$sliderColorKnob: rgba($sliderColorBase, 0.5);
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
$sliderColorRangeValHovBg: $sliderColorRange; //rgba($sliderColorBase, 0.1);
$sliderColorRangeValHovFg: $colorBodyFg;
$sliderKnobW: nth($ueTimeControlH,2)/2;
$timeControllerToiLineColor: $colorBodyFg;
$timeControllerToiLineColorHov: #0052b5;
// General Colors // General Colors
$colorAlt1: #ff6600; $colorAlt1: #776ba2;
$colorAlert: #ff3c00; $colorAlert: #ff3c00;
$colorIconLink: #49dedb; $colorIconLink: #49dedb;
$colorPausedBg: #ff9900; $colorPausedBg: #ff9900;
@ -32,6 +46,7 @@ $colorGridLines: rgba(#000, 0.05);
$colorInvokeMenu: #fff; $colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg; $colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pushBack($colorObjHdrTxt, 30%); $colorObjHdrIc: pushBack($colorObjHdrTxt, 30%);
$colorTick: rgba(black, 0.2);
// Menu colors // Menu colors
$colorMenuBg: pushBack($colorBodyBg, 10%); $colorMenuBg: pushBack($colorBodyBg, 10%);
@ -104,9 +119,10 @@ $colorTabHeaderBorder: $colorBodyBg;
// Plot // Plot
$colorPlotBg: rgba(black, 0.05); $colorPlotBg: rgba(black, 0.05);
$colorPlotFg: $colorBodyFg; $colorPlotFg: $colorBodyFg;
$colorPlotHash: rgba(black, 0.2); $colorPlotHash: $colorTick;
$stylePlotHash: dashed; $stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder; $colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree // Tree
$colorItemTreeIcon: $colorKey; $colorItemTreeIcon: $colorKey;
@ -137,5 +153,16 @@ $colorGrippyInteriorHover: $colorBodyBg;
// Mobile // Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 2%); $colorMobilePaneLeft: darken($colorBodyBg, 2%);
// Datetime Picker, Calendar
$colorCalCellHovBg: $colorKey;
$colorCalCellHovFg: $colorKeyFg;
$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
// About Screen // About Screen
$colorAboutLink: #84b3ff; $colorAboutLink: #84b3ff;
// Loading
$colorLoadingFg: $colorAlt1;
$colorLoadingBg: rgba($colorLoadingFg, 0.1);

View File

@ -1,5 +1,6 @@
@mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg) { @mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg) {
@include containerBase($bg, $fg); @include containerBase($bg, $fg);
@include boxShdw($shdwBtns);
} }
@mixin btnSubtle($bg: $colorBtnBg, $bgHov: none, $fg: $colorBtnFg, $ic: $colorBtnIcon) { @mixin btnSubtle($bg: $colorBtnBg, $bgHov: none, $fg: $colorBtnFg, $ic: $colorBtnIcon) {

View File

@ -66,6 +66,7 @@
"depends": [ "depends": [
"persistenceService", "persistenceService",
"$q", "$q",
"now",
"PERSISTENCE_SPACE", "PERSISTENCE_SPACE",
"ADDITIONAL_PERSISTENCE_SPACES" "ADDITIONAL_PERSISTENCE_SPACES"
] ]

View File

@ -29,7 +29,8 @@ define(
function () { function () {
"use strict"; "use strict";
var TOPIC_PREFIX = "mutation:"; var GENERAL_TOPIC = "mutation",
TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object // Utility function to overwrite a destination object
// with the contents of a source object. // with the contents of a source object.
@ -78,7 +79,11 @@ define(
* @implements {Capability} * @implements {Capability}
*/ */
function MutationCapability(topic, now, domainObject) { function MutationCapability(topic, now, domainObject) {
this.mutationTopic = topic(TOPIC_PREFIX + domainObject.getId()); this.generalMutationTopic =
topic(GENERAL_TOPIC);
this.specificMutationTopic =
topic(TOPIC_PREFIX + domainObject.getId());
this.now = now; this.now = now;
this.domainObject = domainObject; this.domainObject = domainObject;
} }
@ -115,11 +120,17 @@ define(
// mutator function has a temporary copy to work with. // mutator function has a temporary copy to work with.
var domainObject = this.domainObject, var domainObject = this.domainObject,
now = this.now, now = this.now,
t = this.mutationTopic, generalTopic = this.generalMutationTopic,
specificTopic = this.specificMutationTopic,
model = domainObject.getModel(), model = domainObject.getModel(),
clone = JSON.parse(JSON.stringify(model)), clone = JSON.parse(JSON.stringify(model)),
useTimestamp = arguments.length > 1; useTimestamp = arguments.length > 1;
function notifyListeners(model) {
generalTopic.notify(domainObject);
specificTopic.notify(model);
}
// Function to handle copying values to the actual // Function to handle copying values to the actual
function handleMutation(mutationResult) { function handleMutation(mutationResult) {
// If mutation result was undefined, just use // If mutation result was undefined, just use
@ -136,7 +147,7 @@ define(
copyValues(model, result); copyValues(model, result);
} }
model.modified = useTimestamp ? timestamp : now(); model.modified = useTimestamp ? timestamp : now();
t.notify(model); notifyListeners(model);
} }
// Report the result of the mutation // Report the result of the mutation
@ -158,7 +169,7 @@ define(
* @memberof platform/core.MutationCapability# * @memberof platform/core.MutationCapability#
*/ */
MutationCapability.prototype.listen = function (listener) { MutationCapability.prototype.listen = function (listener) {
return this.mutationTopic.listen(listener); return this.specificMutationTopic.listen(listener);
}; };
/** /**

View File

@ -39,14 +39,16 @@ define(
* @param {PersistenceService} persistenceService the service in which * @param {PersistenceService} persistenceService the service in which
* domain object models are persisted. * domain object models are persisted.
* @param $q Angular's $q service, for working with promises * @param $q Angular's $q service, for working with promises
* @param {function} now a function which provides the current time
* @param {string} space the name of the persistence space(s) * @param {string} space the name of the persistence space(s)
* from which models should be retrieved. * from which models should be retrieved.
* @param {string} spaces additional persistence spaces to use * @param {string} spaces additional persistence spaces to use
*/ */
function PersistedModelProvider(persistenceService, $q, space, spaces) { function PersistedModelProvider(persistenceService, $q, now, space, spaces) {
this.persistenceService = persistenceService; this.persistenceService = persistenceService;
this.$q = $q; this.$q = $q;
this.spaces = [space].concat(spaces || []); this.spaces = [space].concat(spaces || []);
this.now = now;
} }
// Take the most recently modified model, for cases where // Take the most recently modified model, for cases where
@ -61,7 +63,9 @@ define(
PersistedModelProvider.prototype.getModels = function (ids) { PersistedModelProvider.prototype.getModels = function (ids) {
var persistenceService = this.persistenceService, var persistenceService = this.persistenceService,
$q = this.$q, $q = this.$q,
spaces = this.spaces; spaces = this.spaces,
space = this.space,
now = this.now;
// Load a single object model from any persistence spaces // Load a single object model from any persistence spaces
function loadModel(id) { function loadModel(id) {
@ -72,11 +76,24 @@ define(
}); });
} }
// Ensure that models read from persistence have some
// sensible timestamp indicating they've been persisted.
function addPersistedTimestamp(model) {
if (model && (model.persisted === undefined)) {
model.persisted = model.modified !== undefined ?
model.modified : now();
}
return model;
}
// Package the result as id->model // Package the result as id->model
function packageResult(models) { function packageResult(models) {
var result = {}; var result = {};
ids.forEach(function (id, index) { ids.forEach(function (id, index) {
result[id] = models[index]; if (models[index]) {
result[id] = addPersistedTimestamp(models[index]);
}
}); });
return result; return result;
} }

View File

@ -61,7 +61,7 @@ define(
* @memberof platform/core.Throttle# * @memberof platform/core.Throttle#
*/ */
return function (fn, delay, apply) { return function (fn, delay, apply) {
var promise, // Promise for the result of throttled function var promise,
args = []; args = [];
function invoke() { function invoke() {

View File

@ -35,6 +35,7 @@ define(
SPACE = "space0", SPACE = "space0",
spaces = [ "space1" ], spaces = [ "space1" ],
modTimes, modTimes,
mockNow,
provider; provider;
function mockPromise(value) { function mockPromise(value) {
@ -55,19 +56,33 @@ define(
beforeEach(function () { beforeEach(function () {
modTimes = {}; modTimes = {};
mockQ = { when: mockPromise, all: mockAll }; mockQ = { when: mockPromise, all: mockAll };
mockPersistenceService = { mockPersistenceService = jasmine.createSpyObj(
readObject: function (space, id) { 'persistenceService',
[
'createObject',
'readObject',
'updateObject',
'deleteObject',
'listSpaces',
'listObjects'
]
);
mockNow = jasmine.createSpy("now");
mockPersistenceService.readObject
.andCallFake(function (space, id) {
return mockPromise({ return mockPromise({
space: space, space: space,
id: id, id: id,
modified: (modTimes[space] || {})[id] modified: (modTimes[space] || {})[id],
persisted: 0
}); });
} });
};
provider = new PersistedModelProvider( provider = new PersistedModelProvider(
mockPersistenceService, mockPersistenceService,
mockQ, mockQ,
mockNow,
SPACE, SPACE,
spaces spaces
); );
@ -81,12 +96,13 @@ define(
}); });
expect(models).toEqual({ expect(models).toEqual({
a: { space: SPACE, id: "a" }, a: { space: SPACE, id: "a", persisted: 0 },
x: { space: SPACE, id: "x" }, x: { space: SPACE, id: "x", persisted: 0 },
zz: { space: SPACE, id: "zz" } zz: { space: SPACE, id: "zz", persisted: 0 }
}); });
}); });
it("reads object models from multiple spaces", function () { it("reads object models from multiple spaces", function () {
var models; var models;
@ -99,9 +115,36 @@ define(
}); });
expect(models).toEqual({ expect(models).toEqual({
a: { space: SPACE, id: "a" }, a: { space: SPACE, id: "a", persisted: 0 },
x: { space: 'space1', id: "x", modified: 12321 }, x: { space: 'space1', id: "x", modified: 12321, persisted: 0 },
zz: { space: SPACE, id: "zz" } zz: { space: SPACE, id: "zz", persisted: 0 }
});
});
it("ensures that persisted timestamps are present", function () {
var mockCallback = jasmine.createSpy("callback"),
testModels = {
a: { modified: 123, persisted: 1984, name: "A" },
b: { persisted: 1977, name: "B" },
c: { modified: 42, name: "C" },
d: { name: "D" }
};
mockPersistenceService.readObject.andCallFake(
function (space, id) {
return mockPromise(testModels[id]);
}
);
mockNow.andReturn(12321);
provider.getModels(Object.keys(testModels)).then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith({
a: { modified: 123, persisted: 1984, name: "A" },
b: { persisted: 1977, name: "B" },
c: { modified: 42, persisted: 42, name: "C" },
d: { persisted: 12321, name: "D" }
}); });
}); });

View File

@ -23,7 +23,23 @@
{ {
"key": "conductorService", "key": "conductorService",
"implementation": "ConductorService.js", "implementation": "ConductorService.js",
"depends": [ "now" ] "depends": [ "now", "TIME_CONDUCTOR_DOMAINS" ]
}
],
"templates": [
{
"key": "time-conductor",
"templateUrl": "templates/time-conductor.html"
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{ "key": "time", "name": "Time" },
{ "key": "yesterday", "name": "Yesterday" }
],
"comment": "Placeholder; to be replaced by inspection of available domains."
} }
] ]
} }

View File

@ -0,0 +1,10 @@
<mct-include key="'time-controller'"
ng-model='ngModel.conductor'>
</mct-include>
<mct-control key="'select'"
ng-model='ngModel'
field="'domain'"
options="ngModel.options"
style="position: absolute; right: 0px; bottom: 46px;"
>
</mct-control>

View File

@ -26,15 +26,9 @@ define(
function () { function () {
"use strict"; "use strict";
var CONDUCTOR_HEIGHT = "100px", var TEMPLATE = [
TEMPLATE = [ "<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
'<div style=', "</mct-include>"
'"position: absolute; bottom: 0; width: 100%; ',
'overflow: hidden; ',
'height: ' + CONDUCTOR_HEIGHT + '">',
"<mct-include key=\"'time-controller'\" ng-model='conductor'>",
"</mct-include>",
'</div>'
].join(''), ].join(''),
THROTTLE_MS = 200, THROTTLE_MS = 200,
GLOBAL_SHOWING = false; GLOBAL_SHOWING = false;
@ -83,7 +77,8 @@ define(
function bounds(start, end) { function bounds(start, end) {
return { return {
start: conductor.displayStart(), start: conductor.displayStart(),
end: conductor.displayEnd() end: conductor.displayEnd(),
domain: conductor.domain()
}; };
} }
@ -94,12 +89,30 @@ define(
} }
function updateConductorInner() { function updateConductorInner() {
conductor.displayStart(conductorScope.conductor.inner.start); var innerBounds = conductorScope.ngModel.conductor.inner;
conductor.displayEnd(conductorScope.conductor.inner.end); conductor.displayStart(innerBounds.start);
conductor.displayEnd(innerBounds.end);
lastObservedBounds = lastObservedBounds || bounds(); lastObservedBounds = lastObservedBounds || bounds();
broadcastBounds(); broadcastBounds();
} }
function updateDomain(value) {
conductor.domain(value);
repScope.$broadcast('telemetry:display:bounds', bounds(
conductor.displayStart(),
conductor.displayEnd(),
conductor.domain()
));
}
// telemetry domain metadata -> option for a select control
function makeOption(domainOption) {
return {
name: domainOption.name,
value: domainOption.key
};
}
broadcastBounds = this.throttle(function () { broadcastBounds = this.throttle(function () {
var newlyObservedBounds = bounds(); var newlyObservedBounds = bounds();
@ -112,12 +125,19 @@ define(
} }
}, THROTTLE_MS); }, THROTTLE_MS);
conductorScope.conductor = { outer: bounds(), inner: bounds() }; conductorScope.ngModel = {};
conductorScope.ngModel.conductor =
{ outer: bounds(), inner: bounds() };
conductorScope.ngModel.options =
conductor.domainOptions().map(makeOption);
conductorScope.ngModel.domain = conductor.domain();
conductorScope conductorScope
.$watch('conductor.inner.start', updateConductorInner); .$watch('ngModel.conductor.inner.start', updateConductorInner);
conductorScope conductorScope
.$watch('conductor.inner.end', updateConductorInner); .$watch('ngModel.conductor.inner.end', updateConductorInner);
conductorScope
.$watch('ngModel.domain', updateDomain);
repScope.$on('telemetry:view', updateConductorInner); repScope.$on('telemetry:view', updateConductorInner);
}; };
@ -141,8 +161,7 @@ define(
this.conductorElement = this.conductorElement =
this.$compile(TEMPLATE)(this.conductorScope()); this.$compile(TEMPLATE)(this.conductorScope());
this.element.after(this.conductorElement[0]); this.element.after(this.conductorElement[0]);
this.element.addClass('abs'); this.element.addClass('l-controls-visible l-time-controller-visible');
this.element.css('bottom', CONDUCTOR_HEIGHT);
GLOBAL_SHOWING = true; GLOBAL_SHOWING = true;
} }
}; };

View File

@ -39,12 +39,15 @@ define(
* @param {Function} now a function which returns the current time * @param {Function} now a function which returns the current time
* as a UNIX timestamp, in milliseconds * as a UNIX timestamp, in milliseconds
*/ */
function ConductorService(now) { function ConductorService(now, domains) {
var initialEnd = var initialEnd =
Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS; Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS;
this.conductor = this.conductor = new TimeConductor(
new TimeConductor(initialEnd - ONE_DAY_IN_MS, initialEnd); initialEnd - ONE_DAY_IN_MS,
initialEnd,
domains
);
} }
/** /**

View File

@ -44,12 +44,14 @@ define(
ConductorTelemetryDecorator.prototype.amendRequests = function (requests) { ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
var conductor = this.conductorService.getConductor(), var conductor = this.conductorService.getConductor(),
start = conductor.displayStart(), start = conductor.displayStart(),
end = conductor.displayEnd(); end = conductor.displayEnd(),
domain = conductor.domain();
function amendRequest(request) { function amendRequest(request) {
request = request || {}; request = request || {};
request.start = start; request.start = start;
request.end = end; request.end = end;
request.domain = domain;
return request; return request;
} }

View File

@ -40,8 +40,10 @@ define(
* @param {number} start the initial start time * @param {number} start the initial start time
* @param {number} end the initial end time * @param {number} end the initial end time
*/ */
function TimeConductor(start, end) { function TimeConductor(start, end, domains) {
this.range = { start: start, end: end }; this.range = { start: start, end: end };
this.domains = domains;
this.activeDomain = domains[0].key;
} }
/** /**
@ -68,6 +70,34 @@ define(
return this.range.end; return this.range.end;
}; };
/**
* Get available domain options which can be used to bound time
* selection.
* @returns {TelemetryDomain[]} available domains
*/
TimeConductor.prototype.domainOptions = function () {
return this.domains;
};
/**
* Get or set (if called with an argument) the active domain.
* @param {string} [key] the key identifying the domain choice
* @returns {TelemetryDomain} the active telemetry domain
*/
TimeConductor.prototype.domain = function (key) {
function matchesKey(domain) {
return domain.key === key;
}
if (arguments.length > 0) {
if (!this.domains.some(matchesKey)) {
throw new Error("Unknown domain " + key);
}
this.activeDomain = key;
}
return this.activeDomain;
};
return TimeConductor; return TimeConductor;
} }
); );

View File

@ -21,12 +21,9 @@
*****************************************************************************/ *****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,afterEach,jasmine*/ /*global define,describe,it,expect,beforeEach,waitsFor,afterEach,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define( define(
["../src/ConductorRepresenter"], ["../src/ConductorRepresenter", "./TestTimeConductor"],
function (ConductorRepresenter) { function (ConductorRepresenter, TestTimeConductor) {
"use strict"; "use strict";
var SCOPE_METHODS = [ var SCOPE_METHODS = [
@ -77,10 +74,7 @@ define(
testViews = [ { someKey: "some value" } ]; testViews = [ { someKey: "some value" } ];
mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS); mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS);
mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS); mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS);
mockConductor = jasmine.createSpyObj( mockConductor = new TestTimeConductor();
'conductor',
[ 'displayStart', 'displayEnd' ]
);
mockCompiledTemplate = jasmine.createSpy('template'); mockCompiledTemplate = jasmine.createSpy('template');
mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS); mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS);
mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS); mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS);
@ -135,11 +129,12 @@ define(
it("exposes conductor state in scope", function () { it("exposes conductor state in scope", function () {
mockConductor.displayStart.andReturn(1977); mockConductor.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984); mockConductor.displayEnd.andReturn(1984);
mockConductor.domain.andReturn('d');
representer.represent(testViews[0], {}); representer.represent(testViews[0], {});
expect(mockNewScope.conductor).toEqual({ expect(mockNewScope.ngModel.conductor).toEqual({
inner: { start: 1977, end: 1984 }, inner: { start: 1977, end: 1984, domain: 'd' },
outer: { start: 1977, end: 1984 } outer: { start: 1977, end: 1984, domain: 'd' }
}); });
}); });
@ -151,17 +146,27 @@ define(
representer.represent(testViews[0], {}); representer.represent(testViews[0], {});
mockNewScope.conductor = testState; mockNewScope.ngModel.conductor = testState;
fireWatch(mockNewScope, 'conductor.inner.start', testState.inner.start); fireWatch(
mockNewScope,
'ngModel.conductor.inner.start',
testState.inner.start
);
expect(mockConductor.displayStart).toHaveBeenCalledWith(42); expect(mockConductor.displayStart).toHaveBeenCalledWith(42);
fireWatch(mockNewScope, 'conductor.inner.end', testState.inner.end); fireWatch(
mockNewScope,
'ngModel.conductor.inner.end',
testState.inner.end
);
expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984); expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
}); });
describe("when bounds are changing", function () { describe("when bounds are changing", function () {
var mockThrottledFn = jasmine.createSpy('throttledFn'), var startWatch = "ngModel.conductor.inner.start",
endWatch = "ngModel.conductor.inner.end",
mockThrottledFn = jasmine.createSpy('throttledFn'),
testBounds; testBounds;
function fireThrottledFn() { function fireThrottledFn() {
@ -172,7 +177,7 @@ define(
mockThrottle.andReturn(mockThrottledFn); mockThrottle.andReturn(mockThrottledFn);
representer.represent(testViews[0], {}); representer.represent(testViews[0], {});
testBounds = { start: 0, end: 1000 }; testBounds = { start: 0, end: 1000 };
mockNewScope.conductor.inner = testBounds; mockNewScope.ngModel.conductor.inner = testBounds;
mockConductor.displayStart.andCallFake(function () { mockConductor.displayStart.andCallFake(function () {
return testBounds.start; return testBounds.start;
}); });
@ -184,14 +189,14 @@ define(
it("does not broadcast while bounds are changing", function () { it("does not broadcast while bounds are changing", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled(); expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100; testBounds.start = 100;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500; testBounds.end = 500;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn(); fireThrottledFn();
testBounds.start = 200; testBounds.start = 200;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 400; testBounds.end = 400;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn(); fireThrottledFn();
expect(mockScope.$broadcast).not.toHaveBeenCalled(); expect(mockScope.$broadcast).not.toHaveBeenCalled();
}); });
@ -199,17 +204,56 @@ define(
it("does broadcast when bounds have stabilized", function () { it("does broadcast when bounds have stabilized", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled(); expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100; testBounds.start = 100;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500; testBounds.end = 500;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn(); fireThrottledFn();
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); fireWatch(mockNewScope, startWatch, testBounds.start);
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn(); fireThrottledFn();
expect(mockScope.$broadcast).toHaveBeenCalled(); expect(mockScope.$broadcast).toHaveBeenCalled();
}); });
}); });
it("exposes domain selection in scope", function () {
representer.represent(testViews[0], null);
expect(mockNewScope.ngModel.domain)
.toEqual(mockConductor.domain());
});
it("exposes domain options in scope", function () {
representer.represent(testViews[0], null);
mockConductor.domainOptions().forEach(function (option, i) {
expect(mockNewScope.ngModel.options[i].value)
.toEqual(option.key);
expect(mockNewScope.ngModel.options[i].name)
.toEqual(option.name);
});
});
it("updates domain selection from scope", function () {
var choice;
representer.represent(testViews[0], null);
// Choose a domain that isn't currently selected
mockNewScope.ngModel.options.forEach(function (option) {
if (option.value !== mockNewScope.ngModel.domain) {
choice = option.value;
}
});
expect(mockConductor.domain)
.not.toHaveBeenCalledWith(choice);
mockNewScope.ngModel.domain = choice;
fireWatch(mockNewScope, "ngModel.domain", choice);
expect(mockConductor.domain)
.toHaveBeenCalledWith(choice);
});
}); });
} }
); );

View File

@ -21,9 +21,6 @@
*****************************************************************************/ *****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ /*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define( define(
["../src/ConductorService"], ["../src/ConductorService"],
function (ConductorService) { function (ConductorService) {
@ -38,7 +35,10 @@ define(
beforeEach(function () { beforeEach(function () {
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockNow.andReturn(TEST_NOW); mockNow.andReturn(TEST_NOW);
conductorService = new ConductorService(mockNow); conductorService = new ConductorService(mockNow, [
{ key: "d1", name: "Domain #1" },
{ key: "d2", name: "Domain #2" }
]);
}); });
it("initializes a time conductor around the current time", function () { it("initializes a time conductor around the current time", function () {

View File

@ -23,8 +23,8 @@
define( define(
["../src/ConductorTelemetryDecorator"], ["../src/ConductorTelemetryDecorator", "./TestTimeConductor"],
function (ConductorTelemetryDecorator) { function (ConductorTelemetryDecorator, TestTimeConductor) {
"use strict"; "use strict";
describe("ConductorTelemetryDecorator", function () { describe("ConductorTelemetryDecorator", function () {
@ -54,10 +54,7 @@ define(
'conductorService', 'conductorService',
['getConductor'] ['getConductor']
); );
mockConductor = jasmine.createSpyObj( mockConductor = new TestTimeConductor();
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockPromise = jasmine.createSpyObj( mockPromise = jasmine.createSpyObj(
'promise', 'promise',
['then'] ['then']
@ -78,10 +75,9 @@ define(
return j * j * j; return j * j * j;
}); });
mockConductor.queryStart.andReturn(-12321);
mockConductor.queryEnd.andReturn(-12321);
mockConductor.displayStart.andReturn(42); mockConductor.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977); mockConductor.displayEnd.andReturn(1977);
mockConductor.domain.andReturn("testDomain");
decorator = new ConductorTelemetryDecorator( decorator = new ConductorTelemetryDecorator(
mockConductorService, mockConductorService,
@ -89,24 +85,72 @@ define(
); );
}); });
it("adds display start/end times to historical requests", function () {
describe("decorates historical requests", function () {
var request;
beforeEach(function () {
decorator.requestTelemetry([{ someKey: "some value" }]);
request = mockTelemetryService.requestTelemetry
.mostRecentCall.args[0][0];
});
it("with start times", function () {
expect(request.start).toEqual(mockConductor.displayStart());
});
it("with end times", function () {
expect(request.end).toEqual(mockConductor.displayEnd());
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
});
});
describe("decorates subscription requests", function () {
var request;
beforeEach(function () {
var mockCallback = jasmine.createSpy('callback');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
request = mockTelemetryService.subscribe
.mostRecentCall.args[1][0];
});
it("with start times", function () {
expect(request.start).toEqual(mockConductor.displayStart());
});
it("with end times", function () {
expect(request.end).toEqual(mockConductor.displayEnd());
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
});
});
it("adds display start/end times & domain selection to historical requests", function () {
decorator.requestTelemetry([{ someKey: "some value" }]); decorator.requestTelemetry([{ someKey: "some value" }]);
expect(mockTelemetryService.requestTelemetry) expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{ .toHaveBeenCalledWith([{
someKey: "some value", someKey: "some value",
start: mockConductor.displayStart(), start: mockConductor.displayStart(),
end: mockConductor.displayEnd() end: mockConductor.displayEnd(),
domain: jasmine.any(String)
}]); }]);
}); });
it("adds display start/end times to subscription requests", function () { it("adds display start/end times & domain selection to subscription requests", function () {
var mockCallback = jasmine.createSpy('callback'); var mockCallback = jasmine.createSpy('callback');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]); decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
expect(mockTelemetryService.subscribe) expect(mockTelemetryService.subscribe)
.toHaveBeenCalledWith(jasmine.any(Function), [{ .toHaveBeenCalledWith(jasmine.any(Function), [{
someKey: "some value", someKey: "some value",
start: mockConductor.displayStart(), start: mockConductor.displayStart(),
end: mockConductor.displayEnd() end: mockConductor.displayEnd(),
domain: jasmine.any(String)
}]); }]);
}); });

View File

@ -0,0 +1,50 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,spyOn*/
define(
["../src/TimeConductor"],
function (TimeConductor) {
'use strict';
function TestTimeConductor() {
var self = this;
TimeConductor.apply(this, [
402514200000,
444546000000,
[
{ key: "domain0", name: "Domain #1" },
{ key: "domain1", name: "Domain #2" }
]
]);
Object.keys(TimeConductor.prototype).forEach(function (method) {
spyOn(self, method).andCallThrough();
});
}
TestTimeConductor.prototype = TimeConductor.prototype;
return TestTimeConductor;
}
);

View File

@ -21,9 +21,6 @@
*****************************************************************************/ *****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ /*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define( define(
["../src/TimeConductor"], ["../src/TimeConductor"],
function (TimeConductor) { function (TimeConductor) {
@ -32,12 +29,17 @@ define(
describe("TimeConductor", function () { describe("TimeConductor", function () {
var testStart, var testStart,
testEnd, testEnd,
testDomains,
conductor; conductor;
beforeEach(function () { beforeEach(function () {
testStart = 42; testStart = 42;
testEnd = 12321; testEnd = 12321;
conductor = new TimeConductor(testStart, testEnd); testDomains = [
{ key: "d1", name: "Domain #1" },
{ key: "d2", name: "Domain #2" }
];
conductor = new TimeConductor(testStart, testEnd, testDomains);
}); });
it("provides accessors for query/display start/end times", function () { it("provides accessors for query/display start/end times", function () {
@ -52,6 +54,25 @@ define(
expect(conductor.displayEnd()).toEqual(4); expect(conductor.displayEnd()).toEqual(4);
}); });
it("exposes domain options", function () {
expect(conductor.domainOptions()).toEqual(testDomains);
});
it("exposes the current domain choice", function () {
expect(conductor.domain()).toEqual(testDomains[0].key);
});
it("allows the domain choice to be changed", function () {
conductor.domain(testDomains[1].key);
expect(conductor.domain()).toEqual(testDomains[1].key);
});
it("throws an error on attempts to set an invalid domain", function () {
expect(function () {
conductor.domain("invalid-domain");
}).toThrow();
});
}); });
} }
); );

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div style="width: 100%; height: 100%;" <div class="l-layout"
ng-controller="LayoutController as controller"> ng-controller="LayoutController as controller">
<div class='frame child-frame panel abs' <div class='frame child-frame panel abs'

View File

@ -105,11 +105,13 @@ define(
index index
); );
setDisplayedValue( if (index >= 0) {
telemetryObject, setDisplayedValue(
telemetrySeries.getRangeValue(index), telemetryObject,
limit && datum && limit.evaluate(datum) telemetrySeries.getRangeValue(index),
); limit && datum && limit.evaluate(datum)
);
}
} }
// Update the displayed value for this object // Update the displayed value for this object

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="abs l-iframe"> <div class="l-iframe abs">
<iframe ng-controller="EmbeddedPageController as ctl" <iframe ng-controller="EmbeddedPageController as ctl"
ng-src="{{ctl.trust(model.url)}}"> ng-src="{{ctl.trust(model.url)}}">
</iframe> </iframe>

View File

@ -119,7 +119,7 @@
<span class="ui-symbol icon">I</span> <span class="ui-symbol icon">I</span>
</a> </a>
<div class="menu-element s-menu menus-to-left" <div class="menu-element s-menu-btn menus-to-left"
ng-if="plot.getModeOptions().length > 1" ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle"> ng-controller="ClickAwayController as toggle">

View File

@ -64,6 +64,16 @@ define(
this.updateTicks(); this.updateTicks();
} }
/**
* Tests whether this subplot has domain data to show for the current pan/zoom level. Absence of domain data
* implies that there is no range data displayed either
* @returns {boolean} true if domain data exists for the current pan/zoom level
*/
SubPlot.prototype.hasDomainData = function() {
return this.panZoomStack
&& this.panZoomStack.getDimensions()[0] > 0;
};
// Utility function for filtering out empty strings. // Utility function for filtering out empty strings.
function isNonEmpty(v) { function isNonEmpty(v) {
return typeof v === 'string' && v !== ""; return typeof v === 'string' && v !== "";
@ -253,7 +263,10 @@ define(
this.hovering = true; this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect(); this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event); this.mousePosition = this.toMousePosition($event);
this.updateHoverCoordinates(); //If there is a domain to display, show hover coordinates, otherwise hover coordinates are meaningless
if (this.hasDomainData()) {
this.updateHoverCoordinates();
}
if (this.marqueeStart) { if (this.marqueeStart) {
this.updateMarqueeBox(); this.updateMarqueeBox();
} }

View File

@ -53,7 +53,8 @@ define(
for (i = 0; i < count; i += 1) { for (i = 0; i < count; i += 1) {
result.push({ result.push({
label: format(i * step + start) //If data to show, display label for each tick line, otherwise show lines but suppress labels.
label: span > 0 ? format(i * step + start) : ''
}); });
} }

View File

@ -157,6 +157,15 @@ define(
); );
}); });
it ("indicates when there is domain data shown", function () {
expect(subplot.hasDomainData()).toEqual(true);
});
it ("indicates when there is no domain data shown", function () {
mockPanZoomStack.getDimensions.andReturn([0,0]);
expect(subplot.hasDomainData()).toEqual(false);
});
it("disallows marquee zoom when start and end Marquee is at the same position", function () { it("disallows marquee zoom when start and end Marquee is at the same position", function () {
expect(mockPanZoomStack.pushPanZoom).not.toHaveBeenCalled(); expect(mockPanZoomStack.pushPanZoom).not.toHaveBeenCalled();

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div <div
class="t-btn l-btn s-btn s-icon-btn s-menu menu-element t-color-palette" class="t-btn l-btn s-btn s-icon-btn s-menu-btn menu-element t-color-palette"
ng-controller="ClickAwayController as toggle" ng-controller="ClickAwayController as toggle"
> >

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="s-menu menu-element" <div class="s-menu-btn menu-element"
ng-controller="ClickAwayController as toggle"> ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span> <span class="l-click-area" ng-click="toggle.toggle()"></span>

View File

@ -45,7 +45,15 @@
"provides": "searchService", "provides": "searchService",
"type": "provider", "type": "provider",
"implementation": "services/GenericSearchProvider.js", "implementation": "services/GenericSearchProvider.js",
"depends": [ "$q", "$timeout", "objectService", "workerService", "GENERIC_SEARCH_ROOTS" ] "depends": [
"$q",
"$log",
"throttle",
"objectService",
"workerService",
"topic",
"GENERIC_SEARCH_ROOTS"
]
}, },
{ {
"provides": "searchService", "provides": "searchService",
@ -61,4 +69,4 @@
} }
] ]
} }
} }

View File

@ -28,61 +28,78 @@ define(
[], [],
function () { function () {
"use strict"; "use strict";
var DEFAULT_MAX_RESULTS = 100, var DEFAULT_MAX_RESULTS = 100,
DEFAULT_TIMEOUT = 1000, DEFAULT_TIMEOUT = 1000,
MAX_CONCURRENT_REQUESTS = 100,
FLUSH_INTERVAL = 0,
stopTime; stopTime;
/** /**
* A search service which searches through domain objects in * A search service which searches through domain objects in
* the filetree without using external search implementations. * the filetree without using external search implementations.
* *
* @constructor * @constructor
* @param $q Angular's $q, for promise consolidation. * @param $q Angular's $q, for promise consolidation.
* @param $timeout Angular's $timeout, for delayed function execution. * @param $log Anglar's $log, for logging.
* @param {Function} throttle a function to throttle function invocations
* @param {ObjectService} objectService The service from which * @param {ObjectService} objectService The service from which
* domain objects can be gotten. * domain objects can be gotten.
* @param {WorkerService} workerService The service which allows * @param {WorkerService} workerService The service which allows
* more easy creation of web workers. * more easy creation of web workers.
* @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root * @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root
* domain objects' IDs. * domain objects' IDs.
*/ */
function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) { function GenericSearchProvider($q, $log, throttle, objectService, workerService, topic, ROOTS) {
var indexed = {}, var indexed = {},
pendingIndex = {},
pendingQueries = {}, pendingQueries = {},
worker = workerService.run('genericSearchWorker'); toRequest = [],
worker = workerService.run('genericSearchWorker'),
mutationTopic = topic("mutation"),
indexingStarted = Date.now(),
pendingRequests = 0,
scheduleFlush;
this.worker = worker; this.worker = worker;
this.pendingQueries = pendingQueries; this.pendingQueries = pendingQueries;
this.$q = $q; this.$q = $q;
// pendingQueries is a dictionary with the key value pairs st // pendingQueries is a dictionary with the key value pairs st
// the key is the timestamp and the value is the promise // the key is the timestamp and the value is the promise
function scheduleIdsForIndexing(ids) {
ids.forEach(function (id) {
if (!indexed[id] && !pendingIndex[id]) {
indexed[id] = true;
pendingIndex[id] = true;
toRequest.push(id);
}
});
scheduleFlush();
}
// Tell the web worker to add a domain object's model to its list of items. // Tell the web worker to add a domain object's model to its list of items.
function indexItem(domainObject) { function indexItem(domainObject) {
var message; var model = domainObject.getModel();
// undefined check worker.postMessage({
if (domainObject && domainObject.getModel) { request: 'index',
// Using model instead of whole domain object because model: model,
// it's a JSON object. id: domainObject.getId()
message = { });
request: 'index',
model: domainObject.getModel(), if (Array.isArray(model.composition)) {
id: domainObject.getId() scheduleIdsForIndexing(model.composition);
};
worker.postMessage(message);
} }
} }
// Handles responses from the web worker. Namely, the results of // Handles responses from the web worker. Namely, the results of
// a search request. // a search request.
function handleResponse(event) { function handleResponse(event) {
var ids = [], var ids = [],
id; id;
// If we have the results from a search // If we have the results from a search
if (event.data.request === 'search') { if (event.data.request === 'search') {
// Convert the ids given from the web worker into domain objects // Convert the ids given from the web worker into domain objects
for (id in event.data.results) { for (id in event.data.results) {
@ -91,7 +108,7 @@ define(
objectService.getObjects(ids).then(function (objects) { objectService.getObjects(ids).then(function (objects) {
var searchResults = [], var searchResults = [],
id; id;
// Create searchResult objects // Create searchResult objects
for (id in objects) { for (id in objects) {
searchResults.push({ searchResults.push({
@ -100,8 +117,8 @@ define(
score: event.data.results[id] score: event.data.results[id]
}); });
} }
// Resove the promise corresponding to this // Resove the promise corresponding to this
pendingQueries[event.data.timestamp].resolve({ pendingQueries[event.data.timestamp].resolve({
hits: searchResults, hits: searchResults,
total: event.data.total, total: event.data.total,
@ -110,83 +127,49 @@ define(
}); });
} }
} }
// Helper function for getItems(). Indexes the tree.
function indexItems(nodes) {
nodes.forEach(function (node) {
var id = node && node.getId && node.getId();
// If we have already indexed this item, stop here
if (indexed[id]) {
return;
}
// Index each item with the web worker
indexItem(node);
indexed[id] = true;
// If this node has children, index those
if (node && node.hasCapability && node.hasCapability('composition')) {
// Make sure that this is async, so doesn't block up page
$timeout(function () {
// Get the children...
node.useCapability('composition').then(function (children) {
$timeout(function () {
// ... then index the children
if (children.constructor === Array) {
indexItems(children);
} else {
indexItems([children]);
}
}, 0);
});
}, 0);
}
// Watch for changes to this item, in case it gets new children
if (node && node.hasCapability && node.hasCapability('mutation')) {
node.getCapability('mutation').listen(function (listener) {
if (listener && listener.composition) {
// If the node was mutated to have children, get the child domain objects
objectService.getObjects(listener.composition).then(function (objectsById) {
var objects = [],
id;
// Get each of the domain objects in objectsById function requestAndIndex(id) {
for (id in objectsById) { pendingRequests += 1;
objects.push(objectsById[id]); objectService.getObjects([id]).then(function (objects) {
} delete pendingIndex[id];
if (objects[id]) {
indexItems(objects); indexItem(objects[id]);
});
}
});
} }
}, function () {
$log.warn("Failed to index domain object " + id);
}).then(function () {
pendingRequests -= 1;
scheduleFlush();
}); });
} }
// Converts the filetree into a list scheduleFlush = throttle(function flush() {
function getItems() { var batchSize =
// Aquire root objects Math.max(MAX_CONCURRENT_REQUESTS - pendingRequests, 0);
objectService.getObjects(ROOTS).then(function (objectsById) {
var objects = [], if (toRequest.length + pendingRequests < 1) {
id; $log.info([
'GenericSearch finished indexing after ',
// Get each of the domain objects in objectsById ((Date.now() - indexingStarted) / 1000).toFixed(2),
for (id in objectsById) { ' seconds.'
objects.push(objectsById[id]); ].join(''));
} } else {
toRequest.splice(-batchSize, batchSize)
// Index all of the roots' descendents .forEach(requestAndIndex);
indexItems(objects); }
}); }, FLUSH_INTERVAL);
}
worker.onmessage = handleResponse; worker.onmessage = handleResponse;
// Index the tree's contents once at the beginning // Index the tree's contents once at the beginning
getItems(); scheduleIdsForIndexing(ROOTS);
// Re-index items when they are mutated
mutationTopic.listen(function (domainObject) {
var id = domainObject.getId();
indexed[id] = false;
scheduleIdsForIndexing([id]);
});
} }
/** /**
@ -266,4 +249,4 @@ define(
return GenericSearchProvider; return GenericSearchProvider;
} }
); );

View File

@ -31,35 +31,67 @@ define(
describe("The generic search provider ", function () { describe("The generic search provider ", function () {
var mockQ, var mockQ,
mockTimeout, mockLog,
mockThrottle,
mockDeferred, mockDeferred,
mockObjectService, mockObjectService,
mockObjectPromise, mockObjectPromise,
mockChainedPromise,
mockDomainObjects, mockDomainObjects,
mockCapability, mockCapability,
mockCapabilityPromise, mockCapabilityPromise,
mockWorkerService, mockWorkerService,
mockWorker, mockWorker,
mockTopic,
mockMutationTopic,
mockRoots = ['root1', 'root2'], mockRoots = ['root1', 'root2'],
mockThrottledFn,
throttledCallCount,
provider, provider,
mockProviderResults; mockProviderResults;
beforeEach(function () { function resolveObjectPromises() {
var i; var i;
for (i = 0; i < mockObjectPromise.then.calls.length; i += 1) {
mockChainedPromise.then.calls[i].args[0](
mockObjectPromise.then.calls[i]
.args[0](mockDomainObjects)
);
}
}
function resolveThrottledFn() {
if (mockThrottledFn.calls.length > throttledCallCount) {
mockThrottle.mostRecentCall.args[0]();
throttledCallCount = mockThrottledFn.calls.length;
}
}
function resolveAsyncTasks() {
resolveThrottledFn();
resolveObjectPromises();
}
beforeEach(function () {
mockQ = jasmine.createSpyObj( mockQ = jasmine.createSpyObj(
"$q", "$q",
[ "defer" ] [ "defer" ]
); );
mockLog = jasmine.createSpyObj(
"$log",
[ "error", "warn", "info", "debug" ]
);
mockDeferred = jasmine.createSpyObj( mockDeferred = jasmine.createSpyObj(
"deferred", "deferred",
[ "resolve", "reject"] [ "resolve", "reject"]
); );
mockDeferred.promise = "mock promise"; mockDeferred.promise = "mock promise";
mockQ.defer.andReturn(mockDeferred); mockQ.defer.andReturn(mockDeferred);
mockTimeout = jasmine.createSpy("$timeout"); mockThrottle = jasmine.createSpy("throttle");
mockThrottledFn = jasmine.createSpy("throttledFn");
throttledCallCount = 0;
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
[ "getObjects" ] [ "getObjects" ]
@ -68,9 +100,14 @@ define(
"promise", "promise",
[ "then", "catch" ] [ "then", "catch" ]
); );
mockChainedPromise = jasmine.createSpyObj(
"chainedPromise",
[ "then" ]
);
mockObjectService.getObjects.andReturn(mockObjectPromise); mockObjectService.getObjects.andReturn(mockObjectPromise);
mockTopic = jasmine.createSpy('topic');
mockWorkerService = jasmine.createSpyObj( mockWorkerService = jasmine.createSpyObj(
"workerService", "workerService",
[ "run" ] [ "run" ]
@ -80,68 +117,109 @@ define(
[ "postMessage" ] [ "postMessage" ]
); );
mockWorkerService.run.andReturn(mockWorker); mockWorkerService.run.andReturn(mockWorker);
mockCapabilityPromise = jasmine.createSpyObj( mockCapabilityPromise = jasmine.createSpyObj(
"promise", "promise",
[ "then", "catch" ] [ "then", "catch" ]
); );
mockDomainObjects = {}; mockDomainObjects = {};
for (i = 0; i < 4; i += 1) { ['a', 'root1', 'root2'].forEach(function (id) {
mockDomainObjects[i] = ( mockDomainObjects[id] = (
jasmine.createSpyObj( jasmine.createSpyObj(
"domainObject", "domainObject",
[ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ] [
"getId",
"getModel",
"hasCapability",
"getCapability",
"useCapability"
]
) )
); );
mockDomainObjects[i].getId.andReturn(i); mockDomainObjects[id].getId.andReturn(id);
mockDomainObjects[i].getCapability.andReturn(mockCapability); mockDomainObjects[id].getCapability.andReturn(mockCapability);
mockDomainObjects[i].useCapability.andReturn(mockCapabilityPromise); mockDomainObjects[id].useCapability.andReturn(mockCapabilityPromise);
} mockDomainObjects[id].getModel.andReturn({});
// Give the first object children });
mockDomainObjects[0].hasCapability.andReturn(true);
mockCapability = jasmine.createSpyObj( mockCapability = jasmine.createSpyObj(
"capability", "capability",
[ "invoke", "listen" ] [ "invoke", "listen" ]
); );
mockCapability.invoke.andReturn(mockCapabilityPromise); mockCapability.invoke.andReturn(mockCapabilityPromise);
mockDomainObjects[0].getCapability.andReturn(mockCapability); mockDomainObjects.a.getCapability.andReturn(mockCapability);
mockMutationTopic = jasmine.createSpyObj(
provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots); 'mutationTopic',
[ 'listen' ]
);
mockTopic.andCallFake(function (key) {
return key === 'mutation' && mockMutationTopic;
});
mockThrottle.andReturn(mockThrottledFn);
mockObjectPromise.then.andReturn(mockChainedPromise);
provider = new GenericSearchProvider(
mockQ,
mockLog,
mockThrottle,
mockObjectService,
mockWorkerService,
mockTopic,
mockRoots
);
}); });
it("indexes tree on initialization", function () { it("indexes tree on initialization", function () {
var i;
resolveThrottledFn();
expect(mockObjectService.getObjects).toHaveBeenCalled(); expect(mockObjectService.getObjects).toHaveBeenCalled();
expect(mockObjectPromise.then).toHaveBeenCalled(); expect(mockObjectPromise.then).toHaveBeenCalled();
// Call through the root-getting part // Call through the root-getting part
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); resolveObjectPromises();
// Call through the children-getting part mockRoots.forEach(function (id) {
mockTimeout.mostRecentCall.args[0](); expect(mockWorker.postMessage).toHaveBeenCalledWith({
// Array argument indicates multiple children request: 'index',
mockCapabilityPromise.then.mostRecentCall.args[0]([]); model: mockDomainObjects[id].getModel(),
mockTimeout.mostRecentCall.args[0](); id: id
// Call again, but for single child });
mockCapabilityPromise.then.mostRecentCall.args[0]({}); });
mockTimeout.mostRecentCall.args[0]();
expect(mockWorker.postMessage).toHaveBeenCalled();
}); });
it("when indexing, listens for composition changes", function () { it("indexes members of composition", function () {
var mockListener = {composition: {}}; mockDomainObjects.root1.getModel.andReturn({
composition: ['a']
// Call indexItems });
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
resolveAsyncTasks();
// Call through listening for changes resolveAsyncTasks();
expect(mockCapability.listen).toHaveBeenCalled();
mockCapability.listen.mostRecentCall.args[0](mockListener); expect(mockWorker.postMessage).toHaveBeenCalledWith({
expect(mockObjectService.getObjects).toHaveBeenCalled(); request: 'index',
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); model: mockDomainObjects.a.getModel(),
id: 'a'
});
}); });
it("listens for changes to mutation", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
mockMutationTopic.listen.mostRecentCall
.args[0](mockDomainObjects.a);
resolveAsyncTasks();
expect(mockWorker.postMessage).toHaveBeenCalledWith({
request: 'index',
model: mockDomainObjects.a.getModel(),
id: mockDomainObjects.a.getId()
});
});
it("sends search queries to the worker", function () { it("sends search queries to the worker", function () {
var timestamp = Date.now(); var timestamp = Date.now();
provider.query(' test "query" ', timestamp, 1, 2); provider.query(' test "query" ', timestamp, 1, 2);
@ -153,20 +231,20 @@ define(
timeout: 2 timeout: 2
}); });
}); });
it("gives an empty result for an empty query", function () { it("gives an empty result for an empty query", function () {
var timestamp = Date.now(), var timestamp = Date.now(),
queryOutput; queryOutput;
queryOutput = provider.query('', timestamp, 1, 2); queryOutput = provider.query('', timestamp, 1, 2);
expect(queryOutput.hits).toEqual([]); expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0); expect(queryOutput.total).toEqual(0);
queryOutput = provider.query(); queryOutput = provider.query();
expect(queryOutput.hits).toEqual([]); expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0); expect(queryOutput.total).toEqual(0);
}); });
it("handles responses from the worker", function () { it("handles responses from the worker", function () {
var timestamp = Date.now(), var timestamp = Date.now(),
event = { event = {
@ -181,13 +259,35 @@ define(
timestamp: timestamp timestamp: timestamp
} }
}; };
provider.query(' test "query" ', timestamp); provider.query(' test "query" ', timestamp);
mockWorker.onmessage(event); mockWorker.onmessage(event);
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
expect(mockDeferred.resolve).toHaveBeenCalled(); expect(mockDeferred.resolve).toHaveBeenCalled();
}); });
it("warns when objects are unavailable", function () {
resolveAsyncTasks();
expect(mockLog.warn).not.toHaveBeenCalled();
mockChainedPromise.then.mostRecentCall.args[0](
mockObjectPromise.then.mostRecentCall.args[1]()
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("throttles the loading of objects to index", function () {
expect(mockObjectService.getObjects).not.toHaveBeenCalled();
resolveThrottledFn();
expect(mockObjectService.getObjects).toHaveBeenCalled();
});
it("logs when all objects have been processed", function () {
expect(mockLog.info).not.toHaveBeenCalled();
resolveAsyncTasks();
resolveThrottledFn();
expect(mockLog.info).toHaveBeenCalled();
});
}); });
} }
); );

View File

@ -110,20 +110,23 @@ define(
* Get the latest telemetry datum for this domain object. This * Get the latest telemetry datum for this domain object. This
* will be from real-time telemetry, unless an index is specified, * will be from real-time telemetry, unless an index is specified,
* in which case it will be pulled from the historical telemetry * in which case it will be pulled from the historical telemetry
* series at the specified index. * series at the specified index. If there is no latest available
* datum, this will return undefined.
* *
* @param {DomainObject} domainObject the object of interest * @param {DomainObject} domainObject the object of interest
* @param {number} [index] the index of the data of interest * @param {number} [index] the index of the data of interest
* @returns {TelemetryDatum} the most recent datum * @returns {TelemetryDatum} the most recent datum
*/ */
self.getDatum = function (telemetryObject, index) { self.getDatum = function (telemetryObject, index) {
function makeNewDatum(series) {
return series ?
subscription.makeDatum(telemetryObject, series, index) :
undefined;
}
return typeof index !== 'number' ? return typeof index !== 'number' ?
subscription.getDatum(telemetryObject) : subscription.getDatum(telemetryObject) :
subscription.makeDatum( makeNewDatum(this.getSeries(telemetryObject));
telemetryObject,
this.getSeries(telemetryObject),
index
);
}; };
return self; return self;