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
commit fc751b1332
67 changed files with 3865 additions and 2097 deletions

View File

@ -103,6 +103,21 @@ This build will:
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
Certain terms are used throughout Open MCT Web with consistent meanings

View File

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

View File

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

View File

@ -4,7 +4,11 @@
{
"implementation": "WatchIndicator.js",
"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 class='object-holder abs vscroll'>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject">
</mct-representation>
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs object-holder">
</mct-representation>
</span>

View File

@ -28,7 +28,9 @@
<mct-split-pane class='contents abs' anchor='left'>
<div class='split-pane-component treeview pane left'>
<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>
<div class='holder search-holder abs'
ng-class="{active: treeModel.search}">

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<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="title-label">Create</span>
</div>

View File

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

View File

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

View File

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

View File

@ -29,6 +29,9 @@
}
.ui-symbol {
&.type-icon {
color: $colorObjHdrIc;
}
&.icon {
color: $colorKey;
&.alert {
@ -41,6 +44,9 @@
font-size: 1.65em;
}
}
&.icon-calendar:after {
content: "\e605";
}
}
.bar .ui-symbol {
@ -52,7 +58,7 @@
display: inline-block;
}
.s-menu .invoke-menu,
.s-menu-btn .invoke-menu,
.icon.major .invoke-menu {
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 */
//position: relative;
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
@include webkitProp(transform, translateY(-50%));
//-webkit-transform: translateY(-50%);
//-ms-transform: translateY(-50%);
//transform: translateY(-50%);
}
@mixin verticalCenterBlock($holderH, $itemH) {
@ -374,22 +375,8 @@
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) {
background-color: rgba($c, $a);
background-color: rgba($c, $a) !important;
}
@mixin tmpBorder($c: #ffcc00, $a: 0.75) {

View File

@ -10,9 +10,6 @@
&.fixed {
font-size: 0.8em;
}
&.scrolling {
overflow: auto;
}
.controls,
label,
.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;
}
@ -295,49 +295,155 @@ label.checkbox.custom {
.slider {
$knobH: 100%; //14px;
$knobW: 12px;
$slotH: 50%;
.slot {
// @include border-radius($basicCr * .75);
@include sliderTrack();
height: $slotH;
//@include sliderTrack();
width: auto;
position: absolute;
top: ($knobH - $slotH) / 2;
top: 0;
right: 0;
bottom: auto;
bottom: 0;
left: 0;
}
.knob {
@include btnSubtle();
@include controlGrippy(rgba(black, 0.3), vertical, 1px, solid);
cursor: ew-resize;
//@include btnSubtle();
//@include controlGrippy(rgba(black, 0.3), vertical, 1px, solid);
@include trans-prop-nice-fade(.25s);
background-color: $sliderColorKnob;
&:hover {
background-color: $sliderColorKnobHov;
}
position: absolute;
height: $knobH;
width: $knobW;
width: $sliderKnobW;
top: 0;
auto: 0;
bottom: auto;
left: auto;
&:before {
top: 1px;
bottom: 3px;
left: ($knobW / 2) - 1;
}
}
.knob-l {
@include border-left-radius($sliderKnobW);
cursor: w-resize;
}
.knob-r {
@include border-right-radius($sliderKnobW);
cursor: e-resize;
}
.range {
background: rgba($colorKey, 0.6);
@include trans-prop-nice-fade(.25s);
background-color: $sliderColorRange;
cursor: ew-resize;
position: absolute;
top: 0;
top: 0; //$tbOffset;
right: auto;
bottom: 0;
left: auto;
height: auto;
width: auto;
&: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.
*****************************************************************************/
/******************************************************** MENU BUTTONS */
.s-menu {
.s-menu-btn {
// Formerly .btn-menu
@extend .s-btn;
span.l-click-area {
@ -62,186 +62,192 @@
/******************************************************** MENUS THEMSELVES */
.menu-element {
$bg: $colorMenuBg;
$fg: $colorMenuFg;
$ic: $colorMenuIc;
cursor: pointer;
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,
.context-menu,
.super-menu {
pointer-events: auto;
ul li {
//padding-left: 25px;
a {
color: $fg;
}
.icon {
color: $ic;
}
.type-icon {
left: $interiorMargin;
}
&:hover .icon {
//color: lighten($ic, 5%);
}
}
}
.s-menu {
@include border-radius($basicCr);
@include containerSubtle($colorMenuBg, $colorMenuFg);
@include boxShdw($shdwMenu);
@include txtShdw($shdwMenuText);
padding: $interiorMarginSm 0;
}
.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 {
.menu {
@extend .s-menu;
display: block;
position: absolute;
z-index: 10;
ul {
@include menuUlReset();
li {
@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;
}
border-top: 1px solid lighten($colorMenuBg, 20%);
color: pullForward($colorMenuBg, 60%);
line-height: $menuLineH;
padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
position: relative;
white-space: nowrap;
&:first-child {
border: none;
}
&:hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
.icon {
color: $colorMenuHovIc;
}
}
&.right {
//@include test(red);
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
.type-icon {
left: $interiorMargin * 2;
}
}
.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 {
pointer-events: none;
.menu,
.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;
height: 200px;
width: 170px;
z-index: 70;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
.context-menu {
}
}
&.go-left .context-menu {
&.go-left .context-menu,
&.go-left .menu {
right: 0;
}
&.go-up .context-menu {
&.go-up .context-menu,
&.go-up .menu {
bottom: 0;
}
}
.context-menu-holder {
pointer-events: none;
height: 200px;
width: 170px;
}
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto;

View File

@ -1,72 +1,155 @@
.l-time-controller {
$inputTxtW: 90px;
$knobW: 9px;
$r1H: 20px;
$r2H: 30px;
$r3H: 10px;
@mixin toiLineHovEffects() {
//@include pulse(.25s);
&:before,
&:after {
background-color: $timeControllerToiLineColorHov;
}
}
position: relative;
margin: $interiorMarginLg 0;
min-width: 400px;
.l-time-controller-visible {
}
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-slider {
font-size: 0.8em;
//font-size: 0.8em;
}
.l-time-range-inputs-holder,
.l-time-range-slider-holder,
.l-time-range-ticks-holder
{
margin-bottom: $interiorMargin;
position: relative;
//@include test();
@include absPosDefault(0, visible);
@include box-sizing(border-box);
top: auto;
}
.l-time-range-slider,
.l-time-range-ticks {
//@include test(red, 0.1);
@include absPosDefault(0, visible);
left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset;
}
.l-time-range-inputs-holder {
height: $r1H;
}
.l-time-range-slider,
.l-time-range-ticks {
left: $inputTxtW; right: $inputTxtW;
//@include test(red);
height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
padding-top: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
.type-icon {
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 {
height: $r2H;
//@include test(green);
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
.range-holder {
@include box-shadow(none);
background: 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 {
height: $r3H;
.l-time-range-ticks {
border-top: 1px solid $colorInteriorBorder;
border-top: 1px solid $colorTick;
.tick {
background-color: $colorInteriorBorder;
background-color: $colorTick;
border:none;
height: 5px;
width: 1px;
margin-left: -1px;
position: absolute;
&:first-child {
margin-left: 0;
}
.l-time-range-tick-label {
color: lighten($colorInteriorBorder, 20%);
font-size: 0.7em;
@include webkitProp(transform, translateX(-50%));
color: $colorPlotLabelFg;
display: inline-block;
font-size: 0.9em;
position: absolute;
margin-left: -0.5 * $tickLblW;
text-align: center;
top: $r3H;
width: $tickLblW;
top: 8px;
white-space: nowrap;
z-index: 2;
}
}
@ -74,31 +157,47 @@
}
.knob {
width: $knobW;
z-index: 2;
.range-value {
$w: 75px;
//@include test();
//@include test($sliderColorRange);
@include trans-prop-nice-fade(.25s);
padding: 0 $rangeValOffset;
position: absolute;
top: 50%;
margin-top: -7px; // Label is 13px high
height: $r2H;
line-height: $r2H;
white-space: nowrap;
width: $w;
}
&:hover .range-value {
color: $colorKey;
color: $sliderColorKnobHov;
}
&.knob-l {
margin-left: $knobW / -2;
//@include border-bottom-left-radius($knobCr); // MOVED TO _CONTROLS.SCSS
margin-left: $knobM;
.range-value {
text-align: right;
right: $knobW + $interiorMargin;
right: $rangeValOffset;
}
}
&.knob-r {
margin-right: $knobW / -2;
//@include border-bottom-right-radius($knobCr);
margin-right: $knobM;
.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
* at runtime from the About dialog for additional information.
*****************************************************************************/
@mixin complexFieldHolder($myW) {
width: $myW + $interiorMargin;
input[type="text"] {
width: $myW;
}
}
.complex.datetime {
span {
display: inline-block;
margin-right: $interiorMargin;
}
/*
.field-hints,
.fields {
}
.field-hints {
}
*/
.fields {
margin-top: $interiorMarginSm 0;
padding: $interiorMarginSm 0;
}
.date {
$myW: 80px;
width: $myW + $interiorMargin;
input {
width: $myW;
}
@include complexFieldHolder(80px);
}
.time.md {
@include complexFieldHolder(60px);
}
.time.sm {
$myW: 40px;
width: $myW + $interiorMargin;
input {
width: $myW;
}
@include complexFieldHolder(40px);
}
}

View File

@ -21,10 +21,13 @@
*****************************************************************************/
.select {
@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;
overflow: hidden;
position: relative;
line-height: $formInputH;
select {
@include appearance(none);
@include box-sizing(border-box);
@ -40,11 +43,8 @@
}
&:after {
@include contextArrow();
pointer-events: none;
color: rgba($colorSelectFg, percentToDecimal($contrastInvokeMenuPercent));
//content:"v";
//display: block;
//font-family: 'symbolsfont';
//pointer-events: none;
position: absolute;
right: $interiorMargin; top: 0;
}

View File

@ -19,24 +19,45 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
@-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(359deg);}
@include keyframes(rotation) {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
@-moz-keyframes rotation {
from {-moz-transform: rotate(0deg);}
to {-moz-transform: rotate(359deg);}
@mixin wait-spinner2($b: 5px, $c: $colorAlt1) {
@include keyframes(rotateCentered) {
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 {
from {-o-transform: rotate(0deg);}
to {-o-transform: rotate(359deg);}
}
@keyframes rotation {
from {transform: rotate(0deg);}
to {transform: rotate(359deg);}
@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;
@include border-radius(100%);
}
.t-wait-spinner,
@ -96,4 +117,28 @@
margin-top: 0 !important;
padding: 0 !important;
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 {
border-bottom: 1px solid $colorTabHeaderBorder;
}
&:not(.fixed-header) tr th {
background-color: $colorTabHeaderBg;
}
tbody, .tbody {
display: table-row-group;
tr, .tr {
@ -64,7 +69,6 @@ table {
display: table-cell;
}
th, .th {
background-color: $colorTabHeaderBg;
border-left: 1px solid $colorTabHeaderBorder;
color: $colorTabHeaderFg;
padding: $tabularTdPadLR $tabularTdPadLR;
@ -143,7 +147,7 @@ table {
position: absolute;
width: 100%;
height: $tabularHeaderH;
background: rgba(#fff, 0.15);
background-color: $colorTabHeaderBg;
}
}
tbody, .tbody {

View File

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

View File

@ -214,8 +214,6 @@
.search-scroll {
order: 3;
//padding-right: $rightPadding;
margin-top: 4px;
// Adjustable scrolling size
@ -227,28 +225,6 @@
.load-icon {
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,8 @@ define(
function (moment) {
"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
@ -34,12 +35,23 @@ define(
*/
function TimeConductorController($scope, now) {
var tickCount = 2,
innerMinimumSpan = 1000, // 1 second
outerMinimumSpan = 1000 * 60 * 60, // 1 hour
initialDragValue;
function formatTimestamp(ts) {
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%"
function toPercent(p) {
return (100 * p) + "%";
@ -59,8 +71,7 @@ define(
}
function updateSpanWidth(w) {
// Space about 100px apart
tickCount = Math.max(Math.floor(w / 100), 2);
tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
updateTicks();
}
@ -90,6 +101,25 @@ define(
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) {
var t = now();
@ -98,8 +128,7 @@ define(
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
// First, dates for the date pickers for outer bounds
$scope.startOuterDate = new Date(ngModel.outer.start);
$scope.endOuterDate = new Date(ngModel.outer.end);
updateBoundsText(ngModel);
// Then various updates for the inner span
updateViewForInnerSpanFromModel(ngModel);
@ -139,7 +168,7 @@ define(
$scope.ngModel.inner.start = clamp(
initialDragValue + delta,
$scope.ngModel.outer.start,
$scope.ngModel.inner.end
$scope.ngModel.inner.end - innerMinimumSpan
);
updateViewFromModel($scope.ngModel);
}
@ -148,7 +177,7 @@ define(
var delta = toMillis(pixels);
$scope.ngModel.inner.end = clamp(
initialDragValue + delta,
$scope.ngModel.inner.start,
$scope.ngModel.inner.start + innerMinimumSpan,
$scope.ngModel.outer.end
);
updateViewFromModel($scope.ngModel);
@ -174,30 +203,76 @@ define(
function updateOuterStart(t) {
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 =
Math.max(ngModel.outer.start, ngModel.inner.start);
ngModel.inner.end =
Math.max(ngModel.outer.start, ngModel.inner.end);
$scope.startOuterText = formatTimestamp(t);
ngModel.inner.end = Math.max(
ngModel.inner.start + innerMinimumSpan,
ngModel.inner.end
);
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
function updateOuterEnd(t) {
var ngModel = $scope.ngModel;
ngModel.outer.start =
Math.min(ngModel.outer.end, ngModel.outer.start);
ngModel.inner.start =
Math.min(ngModel.outer.end, ngModel.inner.start);
ngModel.outer.end = t;
ngModel.outer.start = Math.min(
ngModel.outer.end - outerMinimumSpan,
ngModel.outer.start
);
ngModel.inner.end =
Math.min(ngModel.outer.end, ngModel.inner.end);
$scope.endOuterText = formatTimestamp(t);
ngModel.inner.start = Math.min(
ngModel.inner.end - innerMinimumSpan,
ngModel.inner.start
);
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;
@ -209,14 +284,17 @@ define(
$scope.state = false;
$scope.ticks = [];
$scope.boundsModel = {};
// Initialize scope to defaults
updateViewFromModel($scope.ngModel);
$scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth);
$scope.$watch("ngModel.outer.start", updateOuterStart);
$scope.$watch("ngModel.outer.end", updateOuterEnd);
$scope.$watch("ngModel.outer.start", updateStartFromPicker);
$scope.$watch("ngModel.outer.end", updateEndFromPicker);
$scope.$watch("boundsModel.start", updateStartFromText);
$scope.$watch("boundsModel.end", updateEndFromText);
}
return TimeConductorController;

View File

@ -22,10 +22,15 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/TimeRangeController"],
function (TimeRangeController) {
["../../src/controllers/TimeRangeController", "moment"],
function (TimeRangeController, moment) {
"use strict";
var SEC = 1000,
MIN = 60 * SEC,
HOUR = 60 * MIN,
DAY = 24 * HOUR;
describe("The TimeRangeController", function () {
var mockScope,
mockNow,
@ -61,6 +66,171 @@ define(
.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;
$colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.1);
$colorA: #ccc;
$colorAHov: #fff;
$contrastRatioPercent: 7%;
$basicCr: 2px;
$controlCr: 3px;
$smallCr: 2px;
// Buttons
// Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent); //
$colorBtnFg: $colorBodyFg;
$colorBtnMajorBg: $colorKey;
@ -20,6 +22,18 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: $colorKey;
$colorInvokeMenu: #fff;
$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
$colorAlt1: #ffc700;
@ -34,6 +48,7 @@ $colorFormSectionHeader: rgba(#000, 0.2);
$colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pullForward($colorObjHdrTxt, 20%);
$colorTick: rgba(white, 0.2);
// Menu colors
$colorMenuBg: pullForward($colorBodyBg, 23%);
@ -99,16 +114,17 @@ $colorItemBgSelected: $colorKey;
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: darken($colorBodyBg, 10%);
$colorTabBodyFg: lighten($colorTabBodyBg, 40%);
$colorTabHeaderBg: lighten($colorBodyBg, 10%);
$colorTabHeaderFg: lighten($colorTabHeaderBg, 40%);
$colorTabHeaderBg: rgba(white, 0.1); // lighten($colorBodyBg, 10%);
$colorTabHeaderFg: $colorBodyFg; //lighten($colorTabHeaderBg, 40%);
$colorTabHeaderBorder: $colorBodyBg;
// Plot
$colorPlotBg: rgba(black, 0.1);
$colorPlotFg: $colorBodyFg;
$colorPlotHash: rgba(white, 0.2);
$colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree
$colorItemTreeIcon: $colorKey;
@ -139,5 +155,16 @@ $colorGrippyInteriorHover: $colorKey;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 5%);
// Datetime Picker
$colorCalCellHovBg: $colorKey;
$colorCalCellHovFg: $colorKeyFg;
$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
// 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) {
@include containerBase($bg, $fg);
@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 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%) {

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,14 @@ $colorKey: #0099cc;
$colorKeySelectedBg: $colorKey;
$colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #999;
$colorAHov: $colorKey;
$contrastRatioPercent: 40%;
$basicCr: 4px;
$controlCr: $basicCr;
$smallCr: 3px;
// Buttons
// Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent);
$colorBtnFg: #fff;
$colorBtnMajorBg: $colorKey;
@ -20,9 +22,21 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: #eee;
$colorInvokeMenu: #000;
$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
$colorAlt1: #ff6600;
$colorAlt1: #776ba2;
$colorAlert: #ff3c00;
$colorIconLink: #49dedb;
$colorPausedBg: #ff9900;
@ -32,6 +46,7 @@ $colorGridLines: rgba(#000, 0.05);
$colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pushBack($colorObjHdrTxt, 30%);
$colorTick: rgba(black, 0.2);
// Menu colors
$colorMenuBg: pushBack($colorBodyBg, 10%);
@ -104,9 +119,10 @@ $colorTabHeaderBorder: $colorBodyBg;
// Plot
$colorPlotBg: rgba(black, 0.05);
$colorPlotFg: $colorBodyFg;
$colorPlotHash: rgba(black, 0.2);
$colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree
$colorItemTreeIcon: $colorKey;
@ -137,5 +153,16 @@ $colorGrippyInteriorHover: $colorBodyBg;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 2%);
// Datetime Picker, Calendar
$colorCalCellHovBg: $colorKey;
$colorCalCellHovFg: $colorKeyFg;
$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
// 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) {
@include containerBase($bg, $fg);
@include boxShdw($shdwBtns);
}
@mixin btnSubtle($bg: $colorBtnBg, $bgHov: none, $fg: $colorBtnFg, $ic: $colorBtnIcon) {

View File

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

View File

@ -29,7 +29,8 @@ define(
function () {
"use strict";
var TOPIC_PREFIX = "mutation:";
var GENERAL_TOPIC = "mutation",
TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object
// with the contents of a source object.
@ -78,7 +79,11 @@ define(
* @implements {Capability}
*/
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.domainObject = domainObject;
}
@ -115,11 +120,17 @@ define(
// mutator function has a temporary copy to work with.
var domainObject = this.domainObject,
now = this.now,
t = this.mutationTopic,
generalTopic = this.generalMutationTopic,
specificTopic = this.specificMutationTopic,
model = domainObject.getModel(),
clone = JSON.parse(JSON.stringify(model)),
useTimestamp = arguments.length > 1;
function notifyListeners(model) {
generalTopic.notify(domainObject);
specificTopic.notify(model);
}
// Function to handle copying values to the actual
function handleMutation(mutationResult) {
// If mutation result was undefined, just use
@ -136,7 +147,7 @@ define(
copyValues(model, result);
}
model.modified = useTimestamp ? timestamp : now();
t.notify(model);
notifyListeners(model);
}
// Report the result of the mutation
@ -158,7 +169,7 @@ define(
* @memberof platform/core.MutationCapability#
*/
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
* domain object models are persisted.
* @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)
* from which models should be retrieved.
* @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.$q = $q;
this.spaces = [space].concat(spaces || []);
this.now = now;
}
// Take the most recently modified model, for cases where
@ -61,7 +63,9 @@ define(
PersistedModelProvider.prototype.getModels = function (ids) {
var persistenceService = this.persistenceService,
$q = this.$q,
spaces = this.spaces;
spaces = this.spaces,
space = this.space,
now = this.now;
// Load a single object model from any persistence spaces
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
function packageResult(models) {
var result = {};
ids.forEach(function (id, index) {
result[id] = models[index];
if (models[index]) {
result[id] = addPersistedTimestamp(models[index]);
}
});
return result;
}

View File

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

View File

@ -35,6 +35,7 @@ define(
SPACE = "space0",
spaces = [ "space1" ],
modTimes,
mockNow,
provider;
function mockPromise(value) {
@ -55,19 +56,33 @@ define(
beforeEach(function () {
modTimes = {};
mockQ = { when: mockPromise, all: mockAll };
mockPersistenceService = {
readObject: function (space, id) {
mockPersistenceService = jasmine.createSpyObj(
'persistenceService',
[
'createObject',
'readObject',
'updateObject',
'deleteObject',
'listSpaces',
'listObjects'
]
);
mockNow = jasmine.createSpy("now");
mockPersistenceService.readObject
.andCallFake(function (space, id) {
return mockPromise({
space: space,
id: id,
modified: (modTimes[space] || {})[id]
modified: (modTimes[space] || {})[id],
persisted: 0
});
}
};
});
provider = new PersistedModelProvider(
mockPersistenceService,
mockQ,
mockNow,
SPACE,
spaces
);
@ -81,12 +96,13 @@ define(
});
expect(models).toEqual({
a: { space: SPACE, id: "a" },
x: { space: SPACE, id: "x" },
zz: { space: SPACE, id: "zz" }
a: { space: SPACE, id: "a", persisted: 0 },
x: { space: SPACE, id: "x", persisted: 0 },
zz: { space: SPACE, id: "zz", persisted: 0 }
});
});
it("reads object models from multiple spaces", function () {
var models;
@ -99,9 +115,36 @@ define(
});
expect(models).toEqual({
a: { space: SPACE, id: "a" },
x: { space: 'space1', id: "x", modified: 12321 },
zz: { space: SPACE, id: "zz" }
a: { space: SPACE, id: "a", persisted: 0 },
x: { space: 'space1', id: "x", modified: 12321, persisted: 0 },
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",
"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 () {
"use strict";
var CONDUCTOR_HEIGHT = "100px",
TEMPLATE = [
'<div style=',
'"position: absolute; bottom: 0; width: 100%; ',
'overflow: hidden; ',
'height: ' + CONDUCTOR_HEIGHT + '">',
"<mct-include key=\"'time-controller'\" ng-model='conductor'>",
"</mct-include>",
'</div>'
var TEMPLATE = [
"<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
"</mct-include>"
].join(''),
THROTTLE_MS = 200,
GLOBAL_SHOWING = false;
@ -83,7 +77,8 @@ define(
function bounds(start, end) {
return {
start: conductor.displayStart(),
end: conductor.displayEnd()
end: conductor.displayEnd(),
domain: conductor.domain()
};
}
@ -94,12 +89,30 @@ define(
}
function updateConductorInner() {
conductor.displayStart(conductorScope.conductor.inner.start);
conductor.displayEnd(conductorScope.conductor.inner.end);
var innerBounds = conductorScope.ngModel.conductor.inner;
conductor.displayStart(innerBounds.start);
conductor.displayEnd(innerBounds.end);
lastObservedBounds = lastObservedBounds || bounds();
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 () {
var newlyObservedBounds = bounds();
@ -112,12 +125,19 @@ define(
}
}, 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
.$watch('conductor.inner.start', updateConductorInner);
.$watch('ngModel.conductor.inner.start', updateConductorInner);
conductorScope
.$watch('conductor.inner.end', updateConductorInner);
.$watch('ngModel.conductor.inner.end', updateConductorInner);
conductorScope
.$watch('ngModel.domain', updateDomain);
repScope.$on('telemetry:view', updateConductorInner);
};
@ -141,8 +161,7 @@ define(
this.conductorElement =
this.$compile(TEMPLATE)(this.conductorScope());
this.element.after(this.conductorElement[0]);
this.element.addClass('abs');
this.element.css('bottom', CONDUCTOR_HEIGHT);
this.element.addClass('l-controls-visible l-time-controller-visible');
GLOBAL_SHOWING = true;
}
};

View File

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

View File

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

View File

@ -40,8 +40,10 @@ define(
* @param {number} start the initial start time
* @param {number} end the initial end time
*/
function TimeConductor(start, end) {
function TimeConductor(start, end, domains) {
this.range = { start: start, end: end };
this.domains = domains;
this.activeDomain = domains[0].key;
}
/**
@ -68,6 +70,34 @@ define(
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;
}
);

View File

@ -21,12 +21,9 @@
*****************************************************************************/
/*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(
["../src/ConductorRepresenter"],
function (ConductorRepresenter) {
["../src/ConductorRepresenter", "./TestTimeConductor"],
function (ConductorRepresenter, TestTimeConductor) {
"use strict";
var SCOPE_METHODS = [
@ -77,10 +74,7 @@ define(
testViews = [ { someKey: "some value" } ];
mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS);
mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'displayStart', 'displayEnd' ]
);
mockConductor = new TestTimeConductor();
mockCompiledTemplate = jasmine.createSpy('template');
mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS);
mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS);
@ -135,11 +129,12 @@ define(
it("exposes conductor state in scope", function () {
mockConductor.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984);
mockConductor.domain.andReturn('d');
representer.represent(testViews[0], {});
expect(mockNewScope.conductor).toEqual({
inner: { start: 1977, end: 1984 },
outer: { start: 1977, end: 1984 }
expect(mockNewScope.ngModel.conductor).toEqual({
inner: { start: 1977, end: 1984, domain: 'd' },
outer: { start: 1977, end: 1984, domain: 'd' }
});
});
@ -151,17 +146,27 @@ define(
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);
fireWatch(mockNewScope, 'conductor.inner.end', testState.inner.end);
fireWatch(
mockNewScope,
'ngModel.conductor.inner.end',
testState.inner.end
);
expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
});
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;
function fireThrottledFn() {
@ -172,7 +177,7 @@ define(
mockThrottle.andReturn(mockThrottledFn);
representer.represent(testViews[0], {});
testBounds = { start: 0, end: 1000 };
mockNewScope.conductor.inner = testBounds;
mockNewScope.ngModel.conductor.inner = testBounds;
mockConductor.displayStart.andCallFake(function () {
return testBounds.start;
});
@ -184,14 +189,14 @@ define(
it("does not broadcast while bounds are changing", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start);
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end);
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
testBounds.start = 200;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start);
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 400;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end);
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
expect(mockScope.$broadcast).not.toHaveBeenCalled();
});
@ -199,17 +204,56 @@ define(
it("does broadcast when bounds have stabilized", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100;
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start);
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500;
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end);
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start);
fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end);
fireWatch(mockNewScope, startWatch, testBounds.start);
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
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*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/ConductorService"],
function (ConductorService) {
@ -38,7 +35,10 @@ define(
beforeEach(function () {
mockNow = jasmine.createSpy('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 () {

View File

@ -23,8 +23,8 @@
define(
["../src/ConductorTelemetryDecorator"],
function (ConductorTelemetryDecorator) {
["../src/ConductorTelemetryDecorator", "./TestTimeConductor"],
function (ConductorTelemetryDecorator, TestTimeConductor) {
"use strict";
describe("ConductorTelemetryDecorator", function () {
@ -54,10 +54,7 @@ define(
'conductorService',
['getConductor']
);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockConductor = new TestTimeConductor();
mockPromise = jasmine.createSpyObj(
'promise',
['then']
@ -78,10 +75,9 @@ define(
return j * j * j;
});
mockConductor.queryStart.andReturn(-12321);
mockConductor.queryEnd.andReturn(-12321);
mockConductor.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977);
mockConductor.domain.andReturn("testDomain");
decorator = new ConductorTelemetryDecorator(
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" }]);
expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{
someKey: "some value",
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');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
expect(mockTelemetryService.subscribe)
.toHaveBeenCalledWith(jasmine.any(Function), [{
someKey: "some value",
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*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/TimeConductor"],
function (TimeConductor) {
@ -32,12 +29,17 @@ define(
describe("TimeConductor", function () {
var testStart,
testEnd,
testDomains,
conductor;
beforeEach(function () {
testStart = 42;
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 () {
@ -52,6 +54,25 @@ define(
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
at runtime from the About dialog for additional information.
-->
<div style="width: 100%; height: 100%;"
<div class="l-layout"
ng-controller="LayoutController as controller">
<div class='frame child-frame panel abs'

View File

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

View File

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

View File

@ -119,7 +119,7 @@
<span class="ui-symbol icon">I</span>
</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-controller="ClickAwayController as toggle">

View File

@ -64,6 +64,16 @@ define(
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.
function isNonEmpty(v) {
return typeof v === 'string' && v !== "";
@ -253,7 +263,10 @@ define(
this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect();
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) {
this.updateMarqueeBox();
}

View File

@ -53,7 +53,8 @@ define(
for (i = 0; i < count; i += 1) {
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 () {
expect(mockPanZoomStack.pushPanZoom).not.toHaveBeenCalled();

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<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"
>

View File

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

View File

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

View File

@ -28,61 +28,78 @@ define(
[],
function () {
"use strict";
var DEFAULT_MAX_RESULTS = 100,
DEFAULT_TIMEOUT = 1000,
MAX_CONCURRENT_REQUESTS = 100,
FLUSH_INTERVAL = 0,
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.
*
* @constructor
* @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
* domain objects can be gotten.
* @param {WorkerService} workerService The service which allows
* 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.
*/
function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) {
function GenericSearchProvider($q, $log, throttle, objectService, workerService, topic, ROOTS) {
var indexed = {},
pendingIndex = {},
pendingQueries = {},
worker = workerService.run('genericSearchWorker');
toRequest = [],
worker = workerService.run('genericSearchWorker'),
mutationTopic = topic("mutation"),
indexingStarted = Date.now(),
pendingRequests = 0,
scheduleFlush;
this.worker = worker;
this.pendingQueries = pendingQueries;
this.$q = $q;
// pendingQueries is a dictionary with the key value pairs st
// 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.
function indexItem(domainObject) {
var message;
// undefined check
if (domainObject && domainObject.getModel) {
// Using model instead of whole domain object because
// it's a JSON object.
message = {
request: 'index',
model: domainObject.getModel(),
id: domainObject.getId()
};
worker.postMessage(message);
var model = domainObject.getModel();
worker.postMessage({
request: 'index',
model: model,
id: domainObject.getId()
});
if (Array.isArray(model.composition)) {
scheduleIdsForIndexing(model.composition);
}
}
// Handles responses from the web worker. Namely, the results of
// a search request.
// Handles responses from the web worker. Namely, the results of
// a search request.
function handleResponse(event) {
var ids = [],
id;
// If we have the results from a search
// If we have the results from a search
if (event.data.request === 'search') {
// Convert the ids given from the web worker into domain objects
for (id in event.data.results) {
@ -91,7 +108,7 @@ define(
objectService.getObjects(ids).then(function (objects) {
var searchResults = [],
id;
// Create searchResult objects
for (id in objects) {
searchResults.push({
@ -100,8 +117,8 @@ define(
score: event.data.results[id]
});
}
// Resove the promise corresponding to this
// Resove the promise corresponding to this
pendingQueries[event.data.timestamp].resolve({
hits: searchResults,
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
for (id in objectsById) {
objects.push(objectsById[id]);
}
indexItems(objects);
});
}
});
function requestAndIndex(id) {
pendingRequests += 1;
objectService.getObjects([id]).then(function (objects) {
delete pendingIndex[id];
if (objects[id]) {
indexItem(objects[id]);
}
}, function () {
$log.warn("Failed to index domain object " + id);
}).then(function () {
pendingRequests -= 1;
scheduleFlush();
});
}
// Converts the filetree into a list
function getItems() {
// Aquire root objects
objectService.getObjects(ROOTS).then(function (objectsById) {
var objects = [],
id;
// Get each of the domain objects in objectsById
for (id in objectsById) {
objects.push(objectsById[id]);
}
// Index all of the roots' descendents
indexItems(objects);
});
}
scheduleFlush = throttle(function flush() {
var batchSize =
Math.max(MAX_CONCURRENT_REQUESTS - pendingRequests, 0);
if (toRequest.length + pendingRequests < 1) {
$log.info([
'GenericSearch finished indexing after ',
((Date.now() - indexingStarted) / 1000).toFixed(2),
' seconds.'
].join(''));
} else {
toRequest.splice(-batchSize, batchSize)
.forEach(requestAndIndex);
}
}, FLUSH_INTERVAL);
worker.onmessage = handleResponse;
// Index the tree's contents once at the beginning
getItems();
// Index the tree's contents once at the beginning
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;
}
);
);

View File

@ -31,35 +31,67 @@ define(
describe("The generic search provider ", function () {
var mockQ,
mockTimeout,
mockLog,
mockThrottle,
mockDeferred,
mockObjectService,
mockObjectPromise,
mockChainedPromise,
mockDomainObjects,
mockCapability,
mockCapabilityPromise,
mockWorkerService,
mockWorker,
mockTopic,
mockMutationTopic,
mockRoots = ['root1', 'root2'],
mockThrottledFn,
throttledCallCount,
provider,
mockProviderResults;
beforeEach(function () {
function resolveObjectPromises() {
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(
"$q",
[ "defer" ]
);
mockLog = jasmine.createSpyObj(
"$log",
[ "error", "warn", "info", "debug" ]
);
mockDeferred = jasmine.createSpyObj(
"deferred",
[ "resolve", "reject"]
);
mockDeferred.promise = "mock promise";
mockQ.defer.andReturn(mockDeferred);
mockTimeout = jasmine.createSpy("$timeout");
mockThrottle = jasmine.createSpy("throttle");
mockThrottledFn = jasmine.createSpy("throttledFn");
throttledCallCount = 0;
mockObjectService = jasmine.createSpyObj(
"objectService",
[ "getObjects" ]
@ -68,9 +100,14 @@ define(
"promise",
[ "then", "catch" ]
);
mockChainedPromise = jasmine.createSpyObj(
"chainedPromise",
[ "then" ]
);
mockObjectService.getObjects.andReturn(mockObjectPromise);
mockTopic = jasmine.createSpy('topic');
mockWorkerService = jasmine.createSpyObj(
"workerService",
[ "run" ]
@ -80,68 +117,109 @@ define(
[ "postMessage" ]
);
mockWorkerService.run.andReturn(mockWorker);
mockCapabilityPromise = jasmine.createSpyObj(
"promise",
[ "then", "catch" ]
);
mockDomainObjects = {};
for (i = 0; i < 4; i += 1) {
mockDomainObjects[i] = (
['a', 'root1', 'root2'].forEach(function (id) {
mockDomainObjects[id] = (
jasmine.createSpyObj(
"domainObject",
[ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ]
[
"getId",
"getModel",
"hasCapability",
"getCapability",
"useCapability"
]
)
);
mockDomainObjects[i].getId.andReturn(i);
mockDomainObjects[i].getCapability.andReturn(mockCapability);
mockDomainObjects[i].useCapability.andReturn(mockCapabilityPromise);
}
// Give the first object children
mockDomainObjects[0].hasCapability.andReturn(true);
mockDomainObjects[id].getId.andReturn(id);
mockDomainObjects[id].getCapability.andReturn(mockCapability);
mockDomainObjects[id].useCapability.andReturn(mockCapabilityPromise);
mockDomainObjects[id].getModel.andReturn({});
});
mockCapability = jasmine.createSpyObj(
"capability",
[ "invoke", "listen" ]
);
mockCapability.invoke.andReturn(mockCapabilityPromise);
mockDomainObjects[0].getCapability.andReturn(mockCapability);
provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots);
mockDomainObjects.a.getCapability.andReturn(mockCapability);
mockMutationTopic = jasmine.createSpyObj(
'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 () {
var i;
resolveThrottledFn();
expect(mockObjectService.getObjects).toHaveBeenCalled();
expect(mockObjectPromise.then).toHaveBeenCalled();
// Call through the root-getting part
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
// Call through the children-getting part
mockTimeout.mostRecentCall.args[0]();
// Array argument indicates multiple children
mockCapabilityPromise.then.mostRecentCall.args[0]([]);
mockTimeout.mostRecentCall.args[0]();
// Call again, but for single child
mockCapabilityPromise.then.mostRecentCall.args[0]({});
mockTimeout.mostRecentCall.args[0]();
expect(mockWorker.postMessage).toHaveBeenCalled();
// Call through the root-getting part
resolveObjectPromises();
mockRoots.forEach(function (id) {
expect(mockWorker.postMessage).toHaveBeenCalledWith({
request: 'index',
model: mockDomainObjects[id].getModel(),
id: id
});
});
});
it("when indexing, listens for composition changes", function () {
var mockListener = {composition: {}};
// Call indexItems
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
// Call through listening for changes
expect(mockCapability.listen).toHaveBeenCalled();
mockCapability.listen.mostRecentCall.args[0](mockListener);
expect(mockObjectService.getObjects).toHaveBeenCalled();
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
it("indexes members of composition", function () {
mockDomainObjects.root1.getModel.andReturn({
composition: ['a']
});
resolveAsyncTasks();
resolveAsyncTasks();
expect(mockWorker.postMessage).toHaveBeenCalledWith({
request: 'index',
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 () {
var timestamp = Date.now();
provider.query(' test "query" ', timestamp, 1, 2);
@ -153,20 +231,20 @@ define(
timeout: 2
});
});
it("gives an empty result for an empty query", function () {
var timestamp = Date.now(),
queryOutput;
queryOutput = provider.query('', timestamp, 1, 2);
expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0);
queryOutput = provider.query();
expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0);
});
it("handles responses from the worker", function () {
var timestamp = Date.now(),
event = {
@ -181,13 +259,35 @@ define(
timestamp: timestamp
}
};
provider.query(' test "query" ', timestamp);
mockWorker.onmessage(event);
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
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
* will be from real-time telemetry, unless an index is specified,
* 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 {number} [index] the index of the data of interest
* @returns {TelemetryDatum} the most recent datum
*/
self.getDatum = function (telemetryObject, index) {
function makeNewDatum(series) {
return series ?
subscription.makeDatum(telemetryObject, series, index) :
undefined;
}
return typeof index !== 'number' ?
subscription.getDatum(telemetryObject) :
subscription.makeDatum(
telemetryObject,
this.getSeries(telemetryObject),
index
);
makeNewDatum(this.getSeries(telemetryObject));
};
return self;