mirror of
https://github.com/nasa/openmct.git
synced 2025-06-11 20:01:41 +00:00
Merging in latest github/master
open #90 Squashed commit of the following: commita2d06583ca
Merge:74f289c
5d5425d
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 27 14:04:49 2015 -0700 Merge pull request #216 from nasa/open-vista54a Review and integrate open-vista54a into master commit5d5425db04
Author: Charles Hacskaylo <charlesh88@gmail.com> Date: Tue Oct 27 11:50:16 2015 -0700 [Frontend] Finessing and verifying CSS vista#54 Verified against fixed position and scrolling views using SineWave generator; font-size of glyph tweaked; commita8856c0612
Author: Charles Hacskaylo <charlesh88@gmail.com> Date: Tue Oct 27 11:40:35 2015 -0700 [Frontend] Platform-specific mods to limits vista#54 Refactor limits into multiple classes, separating upr/lwr from red/yellow; Modded SineWaveLimitCapability accordingly; Normalized upr/lwr glyphs; (cherry picked from commit a26d71b) commit74f289cb34
Merge:4ec243c
29bdc9d
Author: akhenry <akhenry@gmail.com> Date: Tue Oct 27 10:48:33 2015 -0700 Merge pull request #206 from nasa/open150b [Plot] Ignore empty lines commit4ec243c6fb
Merge:407d988
3d996ac
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Sat Oct 24 07:48:45 2015 -0700 Merge pull request #212 from nasa/open211 [RequireJS] Specify path for uuid commit407d9881ff
Merge:6ee622b
21739ff
Author: akhenry <akhenry@gmail.com> Date: Fri Oct 23 19:16:51 2015 -0700 Merge pull request #200 from nasa/open-toc [Documentation] Add table of contents commit6ee622b3f5
Merge:099d70b
87e317a
Author: akhenry <akhenry@gmail.com> Date: Fri Oct 23 17:04:04 2015 -0700 Merge pull request #192 from nasa/open153 [CI] Remove non-existent bundle from procfile commit099d70b8d9
Merge:90828ef
8e2a2ee
Author: akhenry <akhenry@gmail.com> Date: Fri Oct 23 17:00:46 2015 -0700 Merge pull request #175 from nasa/open147 [Entanglement] Add "Go To Original" action commit3d996ac466
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 23 16:32:05 2015 -0700 [RequireJS] Specify path for uuid Specify path for uuid, making it available for any code that would require it, without that code needing to know the path to it. Fixes https://github.com/nasa/openmctweb/issues/211. commit90828ef63d
Merge:bf24ac7
dbebf08
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 23 16:23:29 2015 -0700 Merge remote-tracking branch 'github-open/open181' into open-master commit29bdc9d574
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Fri Oct 23 13:04:06 2015 -0700 [Plot] Ignore empty lines Ignore empty lines (plot lines with no data) when determining domain extrema; avoids failure to draw multiple plot lines in a telemetry panel, nasa/openmctweb#150. commitbf24ac7c93
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Fri Oct 23 12:14:46 2015 -0700 [Search] Update field name Update field name in GenericSearchProvider to reflect changes from nasa/openmctweb#193. Avoids exceptions on mutation. Additionally, add test case exercising relevant code and verifying that reindexing is scheduled upon mutation as expected. commit59f094763b
Merge:3080861
496cf85
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Thu Oct 22 16:58:02 2015 -0700 Merge pull request #193 from nasa/search-performance Search performance commitdbebf08500
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 15:38:58 2015 -0700 [Time Controller] Add test cases ...to verify behavior on text entry of dates. commit847c356063
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 15:26:42 2015 -0700 [Time Controller] Change color when input is invalid nasa/openmctweb#181 commit06bcd28558
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 15:22:00 2015 -0700 [Time Controller] Keep inputs in sync Keep inputs in sync with displayed data in time controller, without overwriting user-entered text. nasa/openmctweb#181 commitf88e8ebb51
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 15:08:44 2015 -0700 [Time Controller] Update model state for text entry commit6d2b2fd81e
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 14:46:12 2015 -0700 [Time Controller] Parse user-entered timestamps nasa/openmctweb#181. commit608800ae63
Merge:07818b0
fb0ce1e
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 14:40:42 2015 -0700 Merge remote-tracking branch 'github/master' into open181 Conflicts: platform/commonUI/general/res/templates/controls/time-controller.html commit07818b0a6d
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 14:35:18 2015 -0700 [Time Controller] Show bounds in a text field Show bounds in a text field to allow user editing; supports manual editing of time controller bounds, nasa/openmctweb#181. commit496cf85b7e
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Wed Oct 21 09:46:32 2015 -0700 [JSDoc] Correct mistake commit833f57e284
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Wed Oct 21 07:39:59 2015 -0700 [Search] Don't block UI between requests Timeout subsequent calls to keepIndexing at the end of a indexRequest, so that UI operations are not blocked. commit9a63e99710
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 16:01:42 2015 -0700 [Search] Add spec for ElasticSearchProvider Add spec coverage for ElasticSearchProvider. Also remove unneeded guards for max number of results, as the aggregator will always provide a max number of results. commit21739fffd9
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 20 15:52:49 2015 -0700 [Documentation] Add table of contents Add table of contents to generated documents, without modifying document sources; nasa/openmctweb#189. commit77d81f899b
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:31:33 2015 -0700 [Style] JSLint compliance commitfe3263fdfe
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:27:46 2015 -0700 [Search] Remove invalid specs commitce42429fbd
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:14:43 2015 -0700 [Search] expose constants, add fudge factor The SearchAggregator exposes it's constants to add stability to tests. It also has a fudge factor which increaases the number of results it requests from providers to better support pagination when using client side filtering. commit76151d09a0
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:13:37 2015 -0700 [Search] use service for filters, add spec Add a spec for the SearchController, and use the SearchService to execute filters by supplying a filterPredicate. commitec7e6cc5b4
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 13:55:46 2015 -0700 [Search] Update spec for Generic Search Worker commit1ddce48f7e
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 13:12:04 2015 -0700 [Search] Specs for GenericSearchProvider Write specs for GenericSearchProvider and resolve some implementation bugs they uncovered. commit98b5ff3c77
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 18:14:33 2015 -0700 [Search] Decrement number of pending requests commit14094a48fc
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 17:33:23 2015 -0700 [Search] Remove old specs in prep for rewrite Remove old specs in prep for rewrite. commit8e2a2eeba5
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Mon Oct 19 12:08:49 2015 -0700 [Entanglement] Add license headers ...per code review feedback from nasa/openmctweb#175 commit0f63e4dde9
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 17:06:23 2015 -0700 [Tests] Rewrite search aggregator specs commit12efb47be7
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 16:09:51 2015 -0700 [Search] Remove timeouts and timestamps Remove timeouts and timestamps which were not effectively doing anything. commita2fce8e56c
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 16:05:31 2015 -0700 [Search] Rewrite elasticsearch provider with prototype Rewrite the elasticsearch provider to use prototypes and clean up the implementation. Now returns a modelResults object to keep it in line with the general search provider. commit78e5c0143b
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 15:26:46 2015 -0700 [Search] Overhaul generic search provider Rewrite the generic search provider to use prototypes. Increase performance by utilizing the model service instead of the object service, and use a simplified method of request queueing. commit099591ad2e
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 15:26:04 2015 -0700 [Search] Aggregator returns objects, providers return models Search providers return search results as models for domain objects, as the actual number of max results is enforced by the aggregator, and because the individual providers store and return the models for their objects already. This lowers the amount of resources consumed instantiating domain objects, and also allows the individual search providers to implement function-based filtering on domain object models, which is beneficial as it allows the search filtering in the search controller to be done before paginating of results. commitb5505f372f
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 12:39:41 2015 -0700 [Search] Generic Worker Performance Tweaks The generic search worker now does indexing work during the index operation, ensuring that queries do not have to do extraneous or repeat calculations. Change the return format slightly and fixed a bug in the GenericSearchProvider which caused more objects than intended to be returned from the provider. commit9ad860babd
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 12:34:47 2015 -0700 [Search] Rewrite search controller, tidy Rewrite the search controller, making numerous changes and using prototypical style. First, the search controller immediately hides previous results when a new search is started. Secondly, the search controller ensures that search results displayed match the currently entered query, preventing race conditions. Finally, the search controller uses a poor filtering option that means it may not display all results. commit87e317a6f5
Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 11:33:42 2015 -0700 [CI] Remove non-existent bundle from procfile Remove the example/localstorage bundle from the procfile. Fixes #153. commitbf41d82a78
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:50:35 2015 -0700 [Entanglement] Restore missing specs Restore specs which had been omitted from suite.json (but currently succeed for the relevant scripts); done in the context of nasa/openmctweb#147 commita4944717a1
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:47:37 2015 -0700 [Location] Test getOriginal method commit70bbd3cf97
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:37:37 2015 -0700 [Entanglement] Add test cases for Go To Original commite3afaf0842
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:22:16 2015 -0700 [Entanglement] Add Go To Original nasa/openmctweb#147 commit60f2f9fb6c
Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:08:48 2015 -0700 [Location] Add getOriginal method Add a getOriginal method to the location capability, to simplify loading of original versions of objects. nasa/openmctweb#147
This commit is contained in:
2
Procfile
2
Procfile
@ -1 +1 @@
|
|||||||
web: node app.js --port $PORT --include example/localstorage
|
web: node app.js --port $PORT
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"platform/forms",
|
"platform/forms",
|
||||||
"platform/identity",
|
"platform/identity",
|
||||||
"platform/persistence/local",
|
"platform/persistence/local",
|
||||||
"platform/persistence/queue",
|
"platform/persistence/elastic",
|
||||||
"platform/policy",
|
"platform/policy",
|
||||||
"platform/entanglement",
|
"platform/entanglement",
|
||||||
"platform/search",
|
"platform/search",
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
var CONSTANTS = {
|
var CONSTANTS = {
|
||||||
DIAGRAM_WIDTH: 800,
|
DIAGRAM_WIDTH: 800,
|
||||||
DIAGRAM_HEIGHT: 500
|
DIAGRAM_HEIGHT: 500
|
||||||
};
|
},
|
||||||
|
TOC_HEAD = "# Table of Contents";
|
||||||
|
|
||||||
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
|
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
|
||||||
(function () {
|
(function () {
|
||||||
@ -44,6 +45,7 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
|||||||
split = require("split"),
|
split = require("split"),
|
||||||
stream = require("stream"),
|
stream = require("stream"),
|
||||||
nomnoml = require('nomnoml'),
|
nomnoml = require('nomnoml'),
|
||||||
|
toc = require("markdown-toc"),
|
||||||
Canvas = require('canvas'),
|
Canvas = require('canvas'),
|
||||||
options = require("minimist")(process.argv.slice(2));
|
options = require("minimist")(process.argv.slice(2));
|
||||||
|
|
||||||
@ -110,6 +112,9 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
|||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
transform._flush = function (done) {
|
transform._flush = function (done) {
|
||||||
|
// Prepend table of contents
|
||||||
|
markdown =
|
||||||
|
[ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
|
||||||
this.push("<html><body>\n");
|
this.push("<html><body>\n");
|
||||||
this.push(marked(markdown));
|
this.push(marked(markdown));
|
||||||
this.push("\n</body></html>\n");
|
this.push("\n</body></html>\n");
|
||||||
@ -133,8 +138,8 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
|||||||
customRenderer.link = function (href, title, text) {
|
customRenderer.link = function (href, title, text) {
|
||||||
// ...but only if they look like relative paths
|
// ...but only if they look like relative paths
|
||||||
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
|
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
|
||||||
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
||||||
renderer.link.apply(renderer, arguments);
|
renderer.link.apply(renderer, arguments);
|
||||||
};
|
};
|
||||||
return customRenderer;
|
return customRenderer;
|
||||||
}
|
}
|
||||||
|
@ -30,25 +30,25 @@ define(
|
|||||||
YELLOW = 0.5,
|
YELLOW = 0.5,
|
||||||
LIMITS = {
|
LIMITS = {
|
||||||
rh: {
|
rh: {
|
||||||
cssClass: "s-limit-upr-red",
|
cssClass: "s-limit-upr s-limit-red",
|
||||||
low: RED,
|
low: RED,
|
||||||
high: Number.POSITIVE_INFINITY,
|
high: Number.POSITIVE_INFINITY,
|
||||||
name: "Red High"
|
name: "Red High"
|
||||||
},
|
},
|
||||||
rl: {
|
rl: {
|
||||||
cssClass: "s-limit-lwr-red",
|
cssClass: "s-limit-lwr s-limit-red",
|
||||||
high: -RED,
|
high: -RED,
|
||||||
low: Number.NEGATIVE_INFINITY,
|
low: Number.NEGATIVE_INFINITY,
|
||||||
name: "Red Low"
|
name: "Red Low"
|
||||||
},
|
},
|
||||||
yh: {
|
yh: {
|
||||||
cssClass: "s-limit-upr-yellow",
|
cssClass: "s-limit-upr s-limit-yellow",
|
||||||
low: YELLOW,
|
low: YELLOW,
|
||||||
high: RED,
|
high: RED,
|
||||||
name: "Yellow High"
|
name: "Yellow High"
|
||||||
},
|
},
|
||||||
yl: {
|
yl: {
|
||||||
cssClass: "s-limit-lwr-yellow",
|
cssClass: "s-limit-lwr s-limit-yellow",
|
||||||
low: -RED,
|
low: -RED,
|
||||||
high: -YELLOW,
|
high: -YELLOW,
|
||||||
name: "Yellow Low"
|
name: "Yellow Low"
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"split": "^1.0.0",
|
"split": "^1.0.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"nomnoml": "^0.0.3",
|
"nomnoml": "^0.0.3",
|
||||||
"canvas": "^1.2.7"
|
"canvas": "^1.2.7",
|
||||||
|
"markdown-toc": "^0.11.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"configuration": {
|
||||||
|
"paths": {
|
||||||
|
"uuid": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
* Module defining CreateService. Created by vwoeltje on 11/10/14.
|
* Module defining CreateService. Created by vwoeltje on 11/10/14.
|
||||||
*/
|
*/
|
||||||
define(
|
define(
|
||||||
["../../lib/uuid"],
|
["uuid"],
|
||||||
function (uuid) {
|
function (uuid) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
@ -110,3 +110,8 @@ $dirImgs: $dirCommonRes + 'images/';
|
|||||||
|
|
||||||
/************************** TIMINGS */
|
/************************** TIMINGS */
|
||||||
$controlFadeMs: 100ms;
|
$controlFadeMs: 100ms;
|
||||||
|
|
||||||
|
/************************** LIMITS */
|
||||||
|
$glyphLimit: '\e603';
|
||||||
|
$glyphLimitUpr: '\0000eb';
|
||||||
|
$glyphLimitLwr: '\0000ee';
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
@mixin limit($bg, $ic, $glyph) {
|
@mixin limitGlyph($iconColor, $glyph: $glyphLimit) {
|
||||||
background: $bg !important;
|
&:before {
|
||||||
//color: $fg !important;
|
color: $iconColor;
|
||||||
&:before {
|
content: $glyph;
|
||||||
//@include pulse(1000ms);
|
font-family: symbolsfont;
|
||||||
color: $ic;
|
font-size: 0.8em;
|
||||||
content: $glyph;
|
display: inline;
|
||||||
}
|
margin-right: $interiorMarginSm;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="s-limit"] {
|
.s-limit-red { background: $colorLimitRedBg !important; }
|
||||||
//white-space: nowrap;
|
.s-limit-yellow { background: $colorLimitYellowBg !important; }
|
||||||
&:before {
|
|
||||||
display: inline-block;
|
// Handle limit when applied to a tr
|
||||||
font-family: symbolsfont;
|
tr[class*="s-limit"] {
|
||||||
font-size: 0.75em;
|
&.s-limit-red td:first-child {
|
||||||
font-style: normal !important;
|
@include limitGlyph($colorLimitRedIc);
|
||||||
margin-right: $interiorMarginSm;
|
}
|
||||||
vertical-align: middle;
|
&.s-limit-yellow td:first-child {
|
||||||
}
|
@include limitGlyph($colorLimitYellowIc);
|
||||||
|
}
|
||||||
|
&.s-limit-upr td:first-child:before { content:$glyphLimitUpr; }
|
||||||
|
&.s-limit-lwr td:first-child:before { content:$glyphLimitLwr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-limit-upr-red { @include limit($colorLimitRedBg, $colorLimitRedIc, "\0000eb"); };
|
// Handle limit when applied directly to a non-tr element
|
||||||
.s-limit-upr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ed"); };
|
// Assume this is applied to the element that displays the limit value
|
||||||
.s-limit-lwr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ec"); };
|
:not(tr)[class*="s-limit"] {
|
||||||
.s-limit-lwr-red { @include limit($colorLimitRedBg, $colorLimitRedIc, "\0000ee"); };
|
&.s-limit-red {
|
||||||
|
@include limitGlyph($colorLimitRedIc);
|
||||||
|
}
|
||||||
|
&.s-limit-yellow {
|
||||||
|
@include limitGlyph($colorLimitYellowIc);
|
||||||
|
}
|
||||||
|
&.s-limit-upr:before { content:$glyphLimitUpr; }
|
||||||
|
&.s-limit-lwr:before { content:$glyphLimitLwr; }
|
||||||
|
}
|
@ -19,84 +19,90 @@
|
|||||||
this source code distribution or the Licensing information page available
|
this source code distribution or the Licensing information page available
|
||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<!-- MINE -->
|
|
||||||
<div ng-controller="TimeRangeController">
|
<div ng-controller="TimeRangeController">
|
||||||
<div class="l-time-range-inputs-holder">
|
<div class="l-time-range-inputs-holder">
|
||||||
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</span>
|
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</span>
|
||||||
<span class="l-time-range-input" ng-controller="ToggleController as t1">
|
<span class="l-time-range-input" ng-controller="ToggleController as t1">
|
||||||
<!--<span class="lbl">Start</span>-->
|
<!--<span class="lbl">Start</span>-->
|
||||||
<span class="s-btn time-range-start" ng-click="t1.toggle()">
|
<span class="s-btn time-range-start">
|
||||||
<span class="val">{{startOuterText}}</span>
|
<input type="text"
|
||||||
<a class="ui-symbol icon icon-calendar"></a>
|
ng-model="boundsModel.start"
|
||||||
<mct-popup ng-if="t1.isActive()">
|
ng-class="{ error: !boundsModel.startValid }">
|
||||||
<div mct-click-elsewhere="t1.setState(false)">
|
</input>
|
||||||
<mct-control key="'datetime-picker'"
|
<a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
|
||||||
ng-model="ngModel.outer"
|
<mct-popup ng-if="t1.isActive()">
|
||||||
field="'start'"
|
<div mct-click-elsewhere="t1.setState(false)">
|
||||||
options="{ hours: true }">
|
<mct-control key="'datetime-picker'"
|
||||||
</mct-control>
|
ng-model="ngModel.outer"
|
||||||
</div>
|
field="'start'"
|
||||||
</mct-popup>
|
options="{ hours: true }">
|
||||||
</span>
|
</mct-control>
|
||||||
</span>
|
</div>
|
||||||
|
</mct-popup>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="l-time-range-inputs-elem lbl">to</span>
|
<span class="l-time-range-inputs-elem lbl">to</span>
|
||||||
|
|
||||||
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
||||||
<!--<span class="lbl">End</span>-->
|
<!--<span class="lbl">End</span>-->
|
||||||
<span class="s-btn l-time-range-input" ng-click="t2.toggle()">
|
<span class="s-btn l-time-range-input">
|
||||||
<span class="val">{{endOuterText}}</span>
|
<input type="text"
|
||||||
<a class="ui-symbol icon icon-calendar"></a>
|
ng-model="boundsModel.end"
|
||||||
<mct-popup ng-if="t2.isActive()">
|
ng-class="{ error: !boundsModel.endValid }">
|
||||||
<div mct-click-elsewhere="t2.setState(false)">
|
</input>
|
||||||
<mct-control key="'datetime-picker'"
|
<a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
|
||||||
ng-model="ngModel.outer"
|
</a>
|
||||||
field="'end'"
|
<mct-popup ng-if="t2.isActive()">
|
||||||
options="{ hours: true }">
|
<div mct-click-elsewhere="t2.setState(false)">
|
||||||
</mct-control>
|
<mct-control key="'datetime-picker'"
|
||||||
</div>
|
ng-model="ngModel.outer"
|
||||||
</mct-popup>
|
field="'end'"
|
||||||
</span>
|
options="{ hours: true }">
|
||||||
</span>
|
</mct-control>
|
||||||
</div>
|
</div>
|
||||||
|
</mct-popup>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="l-time-range-slider-holder">
|
<div class="l-time-range-slider-holder">
|
||||||
<div class="l-time-range-slider">
|
<div class="l-time-range-slider">
|
||||||
<div class="slider"
|
<div class="slider"
|
||||||
mct-resize="spanWidth = bounds.width">
|
mct-resize="spanWidth = bounds.width">
|
||||||
<div class="knob knob-l"
|
<div class="knob knob-l"
|
||||||
mct-drag-down="startLeftDrag()"
|
mct-drag-down="startLeftDrag()"
|
||||||
mct-drag="leftDrag(delta[0])"
|
mct-drag="leftDrag(delta[0])"
|
||||||
ng-style="{ left: startInnerPct }">
|
ng-style="{ left: startInnerPct }">
|
||||||
<div class="range-value">{{startInnerText}}</div>
|
<div class="range-value">{{startInnerText}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="knob knob-r"
|
<div class="knob knob-r"
|
||||||
mct-drag-down="startRightDrag()"
|
mct-drag-down="startRightDrag()"
|
||||||
mct-drag="rightDrag(delta[0])"
|
mct-drag="rightDrag(delta[0])"
|
||||||
ng-style="{ right: endInnerPct }">
|
ng-style="{ right: endInnerPct }">
|
||||||
<div class="range-value">{{endInnerText}}</div>
|
<div class="range-value">{{endInnerText}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="slot range-holder">
|
<div class="slot range-holder">
|
||||||
<div class="range"
|
<div class="range"
|
||||||
mct-drag-down="startMiddleDrag()"
|
mct-drag-down="startMiddleDrag()"
|
||||||
mct-drag="middleDrag(delta[0])"
|
mct-drag="middleDrag(delta[0])"
|
||||||
ng-style="{ left: startInnerPct, right: endInnerPct}">
|
ng-style="{ left: startInnerPct, right: endInnerPct}">
|
||||||
<div class="toi-line"></div>
|
<div class="toi-line"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="l-time-range-ticks-holder">
|
<div class="l-time-range-ticks-holder">
|
||||||
<div class="l-time-range-ticks">
|
<div class="l-time-range-ticks">
|
||||||
<div
|
<div
|
||||||
ng-repeat="tick in ticks"
|
ng-repeat="tick in ticks"
|
||||||
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
|
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
|
||||||
class="tick tick-x"
|
class="tick tick-x"
|
||||||
>
|
>
|
||||||
<span class="l-time-range-tick-label">{{tick}}</span>
|
<span class="l-time-range-tick-label">{{tick}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -26,9 +26,8 @@ define(
|
|||||||
function (moment) {
|
function (moment) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var
|
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
||||||
DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
TICK_SPACING_PX = 150;
|
||||||
TICK_SPACING_PX = 150;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @memberof platform/commonUI/general
|
* @memberof platform/commonUI/general
|
||||||
@ -44,6 +43,15 @@ define(
|
|||||||
return moment.utc(ts).format(DATE_FORMAT);
|
return moment.utc(ts).format(DATE_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTimestamp(text) {
|
||||||
|
var m = moment.utc(text, DATE_FORMAT);
|
||||||
|
if (m.isValid()) {
|
||||||
|
return m.valueOf();
|
||||||
|
} else {
|
||||||
|
throw new Error("Could not parse " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// From 0.0-1.0 to "0%"-"1%"
|
// From 0.0-1.0 to "0%"-"1%"
|
||||||
function toPercent(p) {
|
function toPercent(p) {
|
||||||
return (100 * p) + "%";
|
return (100 * p) + "%";
|
||||||
@ -93,6 +101,25 @@ define(
|
|||||||
return { start: bounds.start, end: bounds.end };
|
return { start: bounds.start, end: bounds.end };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBoundsTextForProperty(ngModel, property) {
|
||||||
|
try {
|
||||||
|
if (!$scope.boundsModel[property] ||
|
||||||
|
parseTimestamp($scope.boundsModel[property]) !==
|
||||||
|
ngModel.outer[property]) {
|
||||||
|
$scope.boundsModel[property] =
|
||||||
|
formatTimestamp(ngModel.outer[property]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// User-entered text is invalid, so leave it be
|
||||||
|
// until they fix it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBoundsText(ngModel) {
|
||||||
|
updateBoundsTextForProperty(ngModel, 'start');
|
||||||
|
updateBoundsTextForProperty(ngModel, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
function updateViewFromModel(ngModel) {
|
function updateViewFromModel(ngModel) {
|
||||||
var t = now();
|
var t = now();
|
||||||
|
|
||||||
@ -101,8 +128,7 @@ define(
|
|||||||
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
|
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
|
||||||
|
|
||||||
// First, dates for the date pickers for outer bounds
|
// First, dates for the date pickers for outer bounds
|
||||||
$scope.startOuterDate = new Date(ngModel.outer.start);
|
updateBoundsText(ngModel);
|
||||||
$scope.endOuterDate = new Date(ngModel.outer.end);
|
|
||||||
|
|
||||||
// Then various updates for the inner span
|
// Then various updates for the inner span
|
||||||
updateViewForInnerSpanFromModel(ngModel);
|
updateViewForInnerSpanFromModel(ngModel);
|
||||||
@ -178,6 +204,8 @@ define(
|
|||||||
function updateOuterStart(t) {
|
function updateOuterStart(t) {
|
||||||
var ngModel = $scope.ngModel;
|
var ngModel = $scope.ngModel;
|
||||||
|
|
||||||
|
ngModel.outer.start = t;
|
||||||
|
|
||||||
ngModel.outer.end = Math.max(
|
ngModel.outer.end = Math.max(
|
||||||
ngModel.outer.start + outerMinimumSpan,
|
ngModel.outer.start + outerMinimumSpan,
|
||||||
ngModel.outer.end
|
ngModel.outer.end
|
||||||
@ -190,14 +218,15 @@ define(
|
|||||||
ngModel.inner.end
|
ngModel.inner.end
|
||||||
);
|
);
|
||||||
|
|
||||||
$scope.startOuterText = formatTimestamp(t);
|
|
||||||
|
|
||||||
updateViewForInnerSpanFromModel(ngModel);
|
updateViewForInnerSpanFromModel(ngModel);
|
||||||
|
updateTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOuterEnd(t) {
|
function updateOuterEnd(t) {
|
||||||
var ngModel = $scope.ngModel;
|
var ngModel = $scope.ngModel;
|
||||||
|
|
||||||
|
ngModel.outer.end = t;
|
||||||
|
|
||||||
ngModel.outer.start = Math.min(
|
ngModel.outer.start = Math.min(
|
||||||
ngModel.outer.end - outerMinimumSpan,
|
ngModel.outer.end - outerMinimumSpan,
|
||||||
ngModel.outer.start
|
ngModel.outer.start
|
||||||
@ -210,9 +239,40 @@ define(
|
|||||||
ngModel.inner.start
|
ngModel.inner.start
|
||||||
);
|
);
|
||||||
|
|
||||||
$scope.endOuterText = formatTimestamp(t);
|
|
||||||
|
|
||||||
updateViewForInnerSpanFromModel(ngModel);
|
updateViewForInnerSpanFromModel(ngModel);
|
||||||
|
updateTicks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStartFromText(value) {
|
||||||
|
try {
|
||||||
|
updateOuterStart(parseTimestamp(value));
|
||||||
|
updateBoundsTextForProperty($scope.ngModel, 'end');
|
||||||
|
$scope.boundsModel.startValid = true;
|
||||||
|
} catch (e) {
|
||||||
|
$scope.boundsModel.startValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEndFromText(value) {
|
||||||
|
try {
|
||||||
|
updateOuterEnd(parseTimestamp(value));
|
||||||
|
updateBoundsTextForProperty($scope.ngModel, 'start');
|
||||||
|
$scope.boundsModel.endValid = true;
|
||||||
|
} catch (e) {
|
||||||
|
$scope.boundsModel.endValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStartFromPicker(value) {
|
||||||
|
updateOuterStart(value);
|
||||||
|
updateBoundsText($scope.ngModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEndFromPicker(value) {
|
||||||
|
updateOuterEnd(value);
|
||||||
|
updateBoundsText($scope.ngModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.startLeftDrag = startLeftDrag;
|
$scope.startLeftDrag = startLeftDrag;
|
||||||
@ -224,14 +284,17 @@ define(
|
|||||||
|
|
||||||
$scope.state = false;
|
$scope.state = false;
|
||||||
$scope.ticks = [];
|
$scope.ticks = [];
|
||||||
|
$scope.boundsModel = {};
|
||||||
|
|
||||||
// Initialize scope to defaults
|
// Initialize scope to defaults
|
||||||
updateViewFromModel($scope.ngModel);
|
updateViewFromModel($scope.ngModel);
|
||||||
|
|
||||||
$scope.$watchCollection("ngModel", updateViewFromModel);
|
$scope.$watchCollection("ngModel", updateViewFromModel);
|
||||||
$scope.$watch("spanWidth", updateSpanWidth);
|
$scope.$watch("spanWidth", updateSpanWidth);
|
||||||
$scope.$watch("ngModel.outer.start", updateOuterStart);
|
$scope.$watch("ngModel.outer.start", updateStartFromPicker);
|
||||||
$scope.$watch("ngModel.outer.end", updateOuterEnd);
|
$scope.$watch("ngModel.outer.end", updateEndFromPicker);
|
||||||
|
$scope.$watch("boundsModel.start", updateStartFromText);
|
||||||
|
$scope.$watch("boundsModel.end", updateEndFromText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TimeConductorController;
|
return TimeConductorController;
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
define(
|
define(
|
||||||
["../../src/controllers/TimeRangeController"],
|
["../../src/controllers/TimeRangeController", "moment"],
|
||||||
function (TimeRangeController) {
|
function (TimeRangeController, moment) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var SEC = 1000,
|
var SEC = 1000,
|
||||||
@ -166,8 +166,72 @@ define(
|
|||||||
expect(mockScope.ngModel.inner.end)
|
expect(mockScope.ngModel.inner.end)
|
||||||
.toBeGreaterThan(mockScope.ngModel.inner.start);
|
.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -147,6 +147,7 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
|
|||||||
/************************** CONTROLS */
|
/************************** CONTROLS */
|
||||||
/************************** PATHS */
|
/************************** PATHS */
|
||||||
/************************** TIMINGS */
|
/************************** TIMINGS */
|
||||||
|
/************************** LIMITS */
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
@ -667,45 +668,58 @@ mct-container {
|
|||||||
content: "!"; }
|
content: "!"; }
|
||||||
|
|
||||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||||
[class*="s-limit"]:before {
|
.s-limit-red {
|
||||||
display: inline-block;
|
background: rgba(255, 0, 0, 0.3) !important; }
|
||||||
|
|
||||||
|
/* line 14, ../../../../general/res/sass/_limits.scss */
|
||||||
|
.s-limit-yellow {
|
||||||
|
background: rgba(255, 170, 0, 0.3) !important; }
|
||||||
|
|
||||||
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
tr[class*="s-limit"].s-limit-red td:first-child:before {
|
||||||
|
color: red;
|
||||||
|
content: "";
|
||||||
font-family: symbolsfont;
|
font-family: symbolsfont;
|
||||||
font-size: 0.75em;
|
font-size: 0.8em;
|
||||||
font-style: normal !important;
|
display: inline;
|
||||||
margin-right: 3px;
|
margin-right: 3px; }
|
||||||
vertical-align: middle; }
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
tr[class*="s-limit"].s-limit-yellow td:first-child:before {
|
||||||
/* line 23, ../../../../general/res/sass/_limits.scss */
|
color: #ffaa00;
|
||||||
.s-limit-upr-red {
|
content: "";
|
||||||
background: rgba(255, 0, 0, 0.3) !important; }
|
font-family: symbolsfont;
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
font-size: 0.8em;
|
||||||
.s-limit-upr-red:before {
|
display: inline;
|
||||||
color: red;
|
margin-right: 3px; }
|
||||||
content: "ë"; }
|
|
||||||
|
|
||||||
/* line 24, ../../../../general/res/sass/_limits.scss */
|
/* line 24, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-upr-yellow {
|
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||||
background: rgba(255, 170, 0, 0.3) !important; }
|
content: "ë"; }
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
|
||||||
.s-limit-upr-yellow:before {
|
|
||||||
color: #ffaa00;
|
|
||||||
content: "í"; }
|
|
||||||
|
|
||||||
/* line 25, ../../../../general/res/sass/_limits.scss */
|
/* line 25, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-lwr-yellow {
|
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||||
background: rgba(255, 170, 0, 0.3) !important; }
|
content: "î"; }
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
|
||||||
.s-limit-lwr-yellow:before {
|
|
||||||
color: #ffaa00;
|
|
||||||
content: "ì"; }
|
|
||||||
|
|
||||||
/* line 26, ../../../../general/res/sass/_limits.scss */
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-lwr-red {
|
:not(tr)[class*="s-limit"].s-limit-red:before {
|
||||||
background: rgba(255, 0, 0, 0.3) !important; }
|
color: red;
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
content: "";
|
||||||
.s-limit-lwr-red:before {
|
font-family: symbolsfont;
|
||||||
color: red;
|
font-size: 0.8em;
|
||||||
content: "î"; }
|
display: inline;
|
||||||
|
margin-right: 3px; }
|
||||||
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-yellow:before {
|
||||||
|
color: #ffaa00;
|
||||||
|
content: "";
|
||||||
|
font-family: symbolsfont;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 3px; }
|
||||||
|
/* line 37, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-upr:before {
|
||||||
|
content: "ë"; }
|
||||||
|
/* line 38, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-lwr:before {
|
||||||
|
content: "î"; }
|
||||||
|
|
||||||
/* line 1, ../../../../general/res/sass/_data-status.scss */
|
/* line 1, ../../../../general/res/sass/_data-status.scss */
|
||||||
.s-stale {
|
.s-stale {
|
||||||
@ -4275,13 +4289,45 @@ span.req {
|
|||||||
|
|
||||||
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-hidden .pane.left.treeview {
|
.pane-tree-hidden .pane.left.treeview {
|
||||||
right: 100% !important;
|
-moz-transition-property: opacity;
|
||||||
width: auto !important;
|
-o-transition-property: opacity;
|
||||||
overflow-y: hidden;
|
-webkit-transition-property: opacity;
|
||||||
overflow-x: hidden; }
|
transition-property: opacity;
|
||||||
|
-moz-transition-duration: 150ms;
|
||||||
|
-o-transition-duration: 150ms;
|
||||||
|
-webkit-transition-duration: 150ms;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
-moz-transition-timing-function: ease-in-out;
|
||||||
|
-o-transition-timing-function: ease-in-out;
|
||||||
|
-webkit-transition-timing-function: ease-in-out;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
-moz-transition-delay: 0;
|
||||||
|
-o-transition-delay: 0;
|
||||||
|
-webkit-transition-delay: 0;
|
||||||
|
transition-delay: 0;
|
||||||
|
opacity: 0 !important; }
|
||||||
|
/* line 73, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
|
.pane-tree-hidden .pane.right.items {
|
||||||
|
left: 0 !important; }
|
||||||
|
|
||||||
/* line 82, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 87, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.left.treeview {
|
.pane-tree-showing .pane.left.treeview {
|
||||||
|
-moz-transition-property: opacity;
|
||||||
|
-o-transition-property: opacity;
|
||||||
|
-webkit-transition-property: opacity;
|
||||||
|
transition-property: opacity;
|
||||||
|
-moz-transition-duration: 250ms;
|
||||||
|
-o-transition-duration: 250ms;
|
||||||
|
-webkit-transition-duration: 250ms;
|
||||||
|
transition-duration: 250ms;
|
||||||
|
-moz-transition-timing-function: ease-in-out;
|
||||||
|
-o-transition-timing-function: ease-in-out;
|
||||||
|
-webkit-transition-timing-function: ease-in-out;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
-moz-transition-delay: 250ms;
|
||||||
|
-o-transition-delay: 250ms;
|
||||||
|
-webkit-transition-delay: 250ms;
|
||||||
|
transition-delay: 250ms;
|
||||||
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSI5OCUiIHN0b3AtY29sb3I9IiMwMDAwMDAiIHN0b3Atb3BhY2l0eT0iMC4wIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
|
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSI5OCUiIHN0b3AtY29sb3I9IiMwMDAwMDAiIHN0b3Atb3BhY2l0eT0iMC4wIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-image: -moz-linear-gradient(0deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
background-image: -moz-linear-gradient(0deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
@ -4289,52 +4335,52 @@ span.req {
|
|||||||
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
width: 40% !important; }
|
width: 40% !important; }
|
||||||
/* line 88, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 94, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items {
|
.pane-tree-showing .pane.right.items {
|
||||||
left: 40% !important; }
|
left: 40% !important; }
|
||||||
|
|
||||||
/* line 93, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.toggle-tree {
|
.toggle-tree {
|
||||||
color: #0099cc !important;
|
color: #0099cc !important;
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
left: 10px; }
|
left: 10px; }
|
||||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.toggle-tree:after {
|
.toggle-tree:after {
|
||||||
content: 'm' !important;
|
content: 'm' !important;
|
||||||
font-family: symbolsfont; }
|
font-family: symbolsfont; }
|
||||||
|
|
||||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar {
|
.object-browse-bar {
|
||||||
left: 30px !important; }
|
left: 30px !important; }
|
||||||
/* line 108, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 114, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .context-available {
|
.object-browse-bar .context-available {
|
||||||
opacity: 1 !important; }
|
opacity: 1 !important; }
|
||||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 117, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .view-switcher {
|
.object-browse-bar .view-switcher {
|
||||||
margin-right: 0 !important; }
|
margin-right: 0 !important; }
|
||||||
/* line 113, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 119, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .view-switcher .title-label {
|
.object-browse-bar .view-switcher .title-label {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
||||||
/* line 120, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 126, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.tree-holder {
|
.tree-holder {
|
||||||
overflow-x: hidden !important; }
|
overflow-x: hidden !important; }
|
||||||
|
|
||||||
/* line 124, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 130, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-disable-select {
|
.mobile-disable-select {
|
||||||
-moz-user-select: -moz-none;
|
-moz-user-select: -moz-none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none; }
|
user-select: none; }
|
||||||
|
|
||||||
/* line 129, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 135, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-hide,
|
.mobile-hide,
|
||||||
.mobile-hide-important {
|
.mobile-hide-important {
|
||||||
display: none !important; }
|
display: none !important; }
|
||||||
|
|
||||||
/* line 134, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 140, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-back-hide {
|
.mobile-back-hide {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
-moz-transition-property: opacity;
|
-moz-transition-property: opacity;
|
||||||
@ -4355,7 +4401,7 @@ span.req {
|
|||||||
transition-delay: 0;
|
transition-delay: 0;
|
||||||
opacity: 0; }
|
opacity: 0; }
|
||||||
|
|
||||||
/* line 139, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 145, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-back-unhide {
|
.mobile-back-unhide {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
-moz-transition-property: opacity;
|
-moz-transition-property: opacity;
|
||||||
@ -4376,21 +4422,21 @@ span.req {
|
|||||||
transition-delay: 0;
|
transition-delay: 0;
|
||||||
opacity: 1; } }
|
opacity: 1; } }
|
||||||
@media screen and (orientation: portrait) and (max-width: 514px) and (max-height: 740px) and (max-device-width: 799px) and (max-device-height: 1024px) {
|
@media screen and (orientation: portrait) and (max-width: 514px) and (max-height: 740px) and (max-device-width: 799px) and (max-device-height: 1024px) {
|
||||||
/* line 148, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.left.treeview {
|
.pane-tree-showing .pane.left.treeview {
|
||||||
width: 90% !important; }
|
width: 90% !important; }
|
||||||
/* line 151, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 157, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items {
|
.pane-tree-showing .pane.right.items {
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
-moz-transform: translateX(90%);
|
-moz-transform: translateX(90%);
|
||||||
-ms-transform: translateX(90%);
|
-ms-transform: translateX(90%);
|
||||||
-webkit-transform: translateX(90%);
|
-webkit-transform: translateX(90%);
|
||||||
transform: translateX(90%); }
|
transform: translateX(90%); }
|
||||||
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 160, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items #content-area {
|
.pane-tree-showing .pane.right.items #content-area {
|
||||||
opacity: 0; } }
|
opacity: 0; } }
|
||||||
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
||||||
/* line 162, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 168, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.desktop-hide {
|
.desktop-hide {
|
||||||
display: none; } }
|
display: none; } }
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
|
@ -147,6 +147,7 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
|
|||||||
/************************** CONTROLS */
|
/************************** CONTROLS */
|
||||||
/************************** PATHS */
|
/************************** PATHS */
|
||||||
/************************** TIMINGS */
|
/************************** TIMINGS */
|
||||||
|
/************************** LIMITS */
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
@ -667,45 +668,58 @@ mct-container {
|
|||||||
content: "!"; }
|
content: "!"; }
|
||||||
|
|
||||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||||
[class*="s-limit"]:before {
|
.s-limit-red {
|
||||||
display: inline-block;
|
background: rgba(255, 0, 0, 0.3) !important; }
|
||||||
|
|
||||||
|
/* line 14, ../../../../general/res/sass/_limits.scss */
|
||||||
|
.s-limit-yellow {
|
||||||
|
background: rgba(255, 170, 0, 0.3) !important; }
|
||||||
|
|
||||||
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
tr[class*="s-limit"].s-limit-red td:first-child:before {
|
||||||
|
color: red;
|
||||||
|
content: "";
|
||||||
font-family: symbolsfont;
|
font-family: symbolsfont;
|
||||||
font-size: 0.75em;
|
font-size: 0.8em;
|
||||||
font-style: normal !important;
|
display: inline;
|
||||||
margin-right: 3px;
|
margin-right: 3px; }
|
||||||
vertical-align: middle; }
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
tr[class*="s-limit"].s-limit-yellow td:first-child:before {
|
||||||
/* line 23, ../../../../general/res/sass/_limits.scss */
|
color: #ffaa00;
|
||||||
.s-limit-upr-red {
|
content: "";
|
||||||
background: rgba(255, 0, 0, 0.3) !important; }
|
font-family: symbolsfont;
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
font-size: 0.8em;
|
||||||
.s-limit-upr-red:before {
|
display: inline;
|
||||||
color: red;
|
margin-right: 3px; }
|
||||||
content: "ë"; }
|
|
||||||
|
|
||||||
/* line 24, ../../../../general/res/sass/_limits.scss */
|
/* line 24, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-upr-yellow {
|
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||||
background: rgba(255, 170, 0, 0.3) !important; }
|
content: "ë"; }
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
|
||||||
.s-limit-upr-yellow:before {
|
|
||||||
color: #ffaa00;
|
|
||||||
content: "í"; }
|
|
||||||
|
|
||||||
/* line 25, ../../../../general/res/sass/_limits.scss */
|
/* line 25, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-lwr-yellow {
|
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||||
background: rgba(255, 170, 0, 0.3) !important; }
|
content: "î"; }
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
|
||||||
.s-limit-lwr-yellow:before {
|
|
||||||
color: #ffaa00;
|
|
||||||
content: "ì"; }
|
|
||||||
|
|
||||||
/* line 26, ../../../../general/res/sass/_limits.scss */
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
.s-limit-lwr-red {
|
:not(tr)[class*="s-limit"].s-limit-red:before {
|
||||||
background: rgba(255, 0, 0, 0.3) !important; }
|
color: red;
|
||||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
content: "";
|
||||||
.s-limit-lwr-red:before {
|
font-family: symbolsfont;
|
||||||
color: red;
|
font-size: 0.8em;
|
||||||
content: "î"; }
|
display: inline;
|
||||||
|
margin-right: 3px; }
|
||||||
|
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-yellow:before {
|
||||||
|
color: #ffaa00;
|
||||||
|
content: "";
|
||||||
|
font-family: symbolsfont;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 3px; }
|
||||||
|
/* line 37, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-upr:before {
|
||||||
|
content: "ë"; }
|
||||||
|
/* line 38, ../../../../general/res/sass/_limits.scss */
|
||||||
|
:not(tr)[class*="s-limit"].s-limit-lwr:before {
|
||||||
|
content: "î"; }
|
||||||
|
|
||||||
/* line 1, ../../../../general/res/sass/_data-status.scss */
|
/* line 1, ../../../../general/res/sass/_data-status.scss */
|
||||||
.s-stale {
|
.s-stale {
|
||||||
@ -4216,13 +4230,45 @@ span.req {
|
|||||||
|
|
||||||
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-hidden .pane.left.treeview {
|
.pane-tree-hidden .pane.left.treeview {
|
||||||
right: 100% !important;
|
-moz-transition-property: opacity;
|
||||||
width: auto !important;
|
-o-transition-property: opacity;
|
||||||
overflow-y: hidden;
|
-webkit-transition-property: opacity;
|
||||||
overflow-x: hidden; }
|
transition-property: opacity;
|
||||||
|
-moz-transition-duration: 150ms;
|
||||||
|
-o-transition-duration: 150ms;
|
||||||
|
-webkit-transition-duration: 150ms;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
-moz-transition-timing-function: ease-in-out;
|
||||||
|
-o-transition-timing-function: ease-in-out;
|
||||||
|
-webkit-transition-timing-function: ease-in-out;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
-moz-transition-delay: 0;
|
||||||
|
-o-transition-delay: 0;
|
||||||
|
-webkit-transition-delay: 0;
|
||||||
|
transition-delay: 0;
|
||||||
|
opacity: 0 !important; }
|
||||||
|
/* line 73, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
|
.pane-tree-hidden .pane.right.items {
|
||||||
|
left: 0 !important; }
|
||||||
|
|
||||||
/* line 82, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 87, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.left.treeview {
|
.pane-tree-showing .pane.left.treeview {
|
||||||
|
-moz-transition-property: opacity;
|
||||||
|
-o-transition-property: opacity;
|
||||||
|
-webkit-transition-property: opacity;
|
||||||
|
transition-property: opacity;
|
||||||
|
-moz-transition-duration: 250ms;
|
||||||
|
-o-transition-duration: 250ms;
|
||||||
|
-webkit-transition-duration: 250ms;
|
||||||
|
transition-duration: 250ms;
|
||||||
|
-moz-transition-timing-function: ease-in-out;
|
||||||
|
-o-transition-timing-function: ease-in-out;
|
||||||
|
-webkit-transition-timing-function: ease-in-out;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
-moz-transition-delay: 250ms;
|
||||||
|
-o-transition-delay: 250ms;
|
||||||
|
-webkit-transition-delay: 250ms;
|
||||||
|
transition-delay: 250ms;
|
||||||
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSI5OCUiIHN0b3AtY29sb3I9IiMwMDAwMDAiIHN0b3Atb3BhY2l0eT0iMC4wIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
|
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSI5OCUiIHN0b3AtY29sb3I9IiMwMDAwMDAiIHN0b3Atb3BhY2l0eT0iMC4wIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-image: -moz-linear-gradient(0deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
background-image: -moz-linear-gradient(0deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
@ -4230,52 +4276,52 @@ span.req {
|
|||||||
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 98%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
width: 40% !important; }
|
width: 40% !important; }
|
||||||
/* line 88, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 94, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items {
|
.pane-tree-showing .pane.right.items {
|
||||||
left: 40% !important; }
|
left: 40% !important; }
|
||||||
|
|
||||||
/* line 93, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.toggle-tree {
|
.toggle-tree {
|
||||||
color: #0099cc !important;
|
color: #0099cc !important;
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
left: 10px; }
|
left: 10px; }
|
||||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.toggle-tree:after {
|
.toggle-tree:after {
|
||||||
content: 'm' !important;
|
content: 'm' !important;
|
||||||
font-family: symbolsfont; }
|
font-family: symbolsfont; }
|
||||||
|
|
||||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar {
|
.object-browse-bar {
|
||||||
left: 30px !important; }
|
left: 30px !important; }
|
||||||
/* line 108, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 114, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .context-available {
|
.object-browse-bar .context-available {
|
||||||
opacity: 1 !important; }
|
opacity: 1 !important; }
|
||||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 117, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .view-switcher {
|
.object-browse-bar .view-switcher {
|
||||||
margin-right: 0 !important; }
|
margin-right: 0 !important; }
|
||||||
/* line 113, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 119, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.object-browse-bar .view-switcher .title-label {
|
.object-browse-bar .view-switcher .title-label {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
||||||
/* line 120, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 126, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.tree-holder {
|
.tree-holder {
|
||||||
overflow-x: hidden !important; }
|
overflow-x: hidden !important; }
|
||||||
|
|
||||||
/* line 124, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 130, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-disable-select {
|
.mobile-disable-select {
|
||||||
-moz-user-select: -moz-none;
|
-moz-user-select: -moz-none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none; }
|
user-select: none; }
|
||||||
|
|
||||||
/* line 129, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 135, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-hide,
|
.mobile-hide,
|
||||||
.mobile-hide-important {
|
.mobile-hide-important {
|
||||||
display: none !important; }
|
display: none !important; }
|
||||||
|
|
||||||
/* line 134, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 140, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-back-hide {
|
.mobile-back-hide {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
-moz-transition-property: opacity;
|
-moz-transition-property: opacity;
|
||||||
@ -4296,7 +4342,7 @@ span.req {
|
|||||||
transition-delay: 0;
|
transition-delay: 0;
|
||||||
opacity: 0; }
|
opacity: 0; }
|
||||||
|
|
||||||
/* line 139, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 145, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.mobile-back-unhide {
|
.mobile-back-unhide {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
-moz-transition-property: opacity;
|
-moz-transition-property: opacity;
|
||||||
@ -4317,21 +4363,21 @@ span.req {
|
|||||||
transition-delay: 0;
|
transition-delay: 0;
|
||||||
opacity: 1; } }
|
opacity: 1; } }
|
||||||
@media screen and (orientation: portrait) and (max-width: 514px) and (max-height: 740px) and (max-device-width: 799px) and (max-device-height: 1024px) {
|
@media screen and (orientation: portrait) and (max-width: 514px) and (max-height: 740px) and (max-device-width: 799px) and (max-device-height: 1024px) {
|
||||||
/* line 148, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.left.treeview {
|
.pane-tree-showing .pane.left.treeview {
|
||||||
width: 90% !important; }
|
width: 90% !important; }
|
||||||
/* line 151, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 157, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items {
|
.pane-tree-showing .pane.right.items {
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
-moz-transform: translateX(90%);
|
-moz-transform: translateX(90%);
|
||||||
-ms-transform: translateX(90%);
|
-ms-transform: translateX(90%);
|
||||||
-webkit-transform: translateX(90%);
|
-webkit-transform: translateX(90%);
|
||||||
transform: translateX(90%); }
|
transform: translateX(90%); }
|
||||||
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 160, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.pane-tree-showing .pane.right.items #content-area {
|
.pane-tree-showing .pane.right.items #content-area {
|
||||||
opacity: 0; } }
|
opacity: 0; } }
|
||||||
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
||||||
/* line 162, ../../../../general/res/sass/mobile/_layout.scss */
|
/* line 168, ../../../../general/res/sass/mobile/_layout.scss */
|
||||||
.desktop-hide {
|
.desktop-hide {
|
||||||
display: none; } }
|
display: none; } }
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
|
@ -30,6 +30,14 @@
|
|||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"implementation": "actions/LinkAction.js",
|
"implementation": "actions/LinkAction.js",
|
||||||
"depends": ["locationService", "linkService"]
|
"depends": ["locationService", "linkService"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "follow",
|
||||||
|
"name": "Go To Original",
|
||||||
|
"description": "Go to the original, un-linked instance of this object.",
|
||||||
|
"glyph": "\u00F4",
|
||||||
|
"category": "contextual",
|
||||||
|
"implementation": "actions/GoToOriginalAction.js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@ -52,7 +60,8 @@
|
|||||||
"key": "location",
|
"key": "location",
|
||||||
"name": "Location Capability",
|
"name": "Location Capability",
|
||||||
"description": "Provides a capability for retrieving the location of an object based upon it's context.",
|
"description": "Provides a capability for retrieving the location of an object based upon it's context.",
|
||||||
"implementation": "capabilities/LocationCapability"
|
"implementation": "capabilities/LocationCapability",
|
||||||
|
"depends": [ "$q", "$injector" ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": [
|
"services": [
|
||||||
|
62
platform/entanglement/src/actions/GoToOriginalAction.js
Normal file
62
platform/entanglement/src/actions/GoToOriginalAction.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the "Go To Original" action, which follows a link back
|
||||||
|
* to an original instance of an object.
|
||||||
|
*
|
||||||
|
* @implements {Action}
|
||||||
|
* @constructor
|
||||||
|
* @private
|
||||||
|
* @memberof platform/entanglement
|
||||||
|
* @param {ActionContext} context the context in which the action
|
||||||
|
* will be performed
|
||||||
|
*/
|
||||||
|
function GoToOriginalAction(context) {
|
||||||
|
this.domainObject = context.domainObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
GoToOriginalAction.prototype.perform = function () {
|
||||||
|
return this.domainObject.getCapability("location").getOriginal()
|
||||||
|
.then(function (originalObject) {
|
||||||
|
var actionCapability =
|
||||||
|
originalObject.getCapability("action");
|
||||||
|
return actionCapability &&
|
||||||
|
actionCapability.perform("navigate");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
GoToOriginalAction.appliesTo = function (context) {
|
||||||
|
var domainObject = context.domainObject;
|
||||||
|
return domainObject && domainObject.hasCapability("location")
|
||||||
|
&& domainObject.getCapability("location").isLink();
|
||||||
|
};
|
||||||
|
|
||||||
|
return GoToOriginalAction;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -1,3 +1,25 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 */
|
/*global define */
|
||||||
|
|
||||||
define(
|
define(
|
||||||
@ -12,11 +34,41 @@ define(
|
|||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function LocationCapability(domainObject) {
|
function LocationCapability($q, $injector, domainObject) {
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
|
this.$q = $q;
|
||||||
|
this.$injector = $injector;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an instance of this domain object in its original location.
|
||||||
|
*
|
||||||
|
* @returns {Promise.<DomainObject>} a promise for the original
|
||||||
|
* instance of this domain object
|
||||||
|
*/
|
||||||
|
LocationCapability.prototype.getOriginal = function () {
|
||||||
|
var id;
|
||||||
|
|
||||||
|
if (this.isOriginal()) {
|
||||||
|
return this.$q.when(this.domainObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
id = this.domainObject.getId();
|
||||||
|
|
||||||
|
this.objectService =
|
||||||
|
this.objectService || this.$injector.get("objectService");
|
||||||
|
|
||||||
|
// Assume that an object will be correctly contextualized when
|
||||||
|
// loaded directly from the object service; this is true
|
||||||
|
// so long as LocatingObjectDecorator is present, and that
|
||||||
|
// decorator is also contained in this bundle.
|
||||||
|
return this.objectService.getObjects([id])
|
||||||
|
.then(function (objects) {
|
||||||
|
return objects[id];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the primary location (the parent id) of the current domain
|
* Set the primary location (the parent id) of the current domain
|
||||||
* object.
|
* object.
|
||||||
@ -78,10 +130,6 @@ define(
|
|||||||
return !this.isLink();
|
return !this.isLink();
|
||||||
};
|
};
|
||||||
|
|
||||||
function createLocationCapability(domainObject) {
|
return LocationCapability;
|
||||||
return new LocationCapability(domainObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createLocationCapability;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
95
platform/entanglement/test/actions/GoToOriginalActionSpec.js
Normal file
95
platform/entanglement/test/actions/GoToOriginalActionSpec.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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,beforeEach,it,jasmine,expect */
|
||||||
|
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'../../src/actions/GoToOriginalAction',
|
||||||
|
'../DomainObjectFactory',
|
||||||
|
'../ControlledPromise'
|
||||||
|
],
|
||||||
|
function (GoToOriginalAction, domainObjectFactory, ControlledPromise) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe("The 'go to original' action", function () {
|
||||||
|
var testContext,
|
||||||
|
originalDomainObject,
|
||||||
|
mockLocationCapability,
|
||||||
|
mockOriginalActionCapability,
|
||||||
|
originalPromise,
|
||||||
|
action;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockLocationCapability = jasmine.createSpyObj(
|
||||||
|
'location',
|
||||||
|
[ 'isLink', 'isOriginal', 'getOriginal' ]
|
||||||
|
);
|
||||||
|
mockOriginalActionCapability = jasmine.createSpyObj(
|
||||||
|
'action',
|
||||||
|
[ 'perform', 'getActions' ]
|
||||||
|
);
|
||||||
|
originalPromise = new ControlledPromise();
|
||||||
|
mockLocationCapability.getOriginal.andReturn(originalPromise);
|
||||||
|
mockLocationCapability.isLink.andReturn(true);
|
||||||
|
mockLocationCapability.isOriginal.andCallFake(function () {
|
||||||
|
return !mockLocationCapability.isLink();
|
||||||
|
});
|
||||||
|
testContext = {
|
||||||
|
domainObject: domainObjectFactory({
|
||||||
|
capabilities: {
|
||||||
|
location: mockLocationCapability
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
originalDomainObject = domainObjectFactory({
|
||||||
|
capabilities: {
|
||||||
|
action: mockOriginalActionCapability
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action = new GoToOriginalAction(testContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is applicable to links", function () {
|
||||||
|
expect(GoToOriginalAction.appliesTo(testContext))
|
||||||
|
.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is not applicable to originals", function () {
|
||||||
|
mockLocationCapability.isLink.andReturn(false);
|
||||||
|
expect(GoToOriginalAction.appliesTo(testContext))
|
||||||
|
.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to original objects when performed", function () {
|
||||||
|
expect(mockOriginalActionCapability.perform)
|
||||||
|
.not.toHaveBeenCalled();
|
||||||
|
action.perform();
|
||||||
|
originalPromise.resolve(originalDomainObject);
|
||||||
|
expect(mockOriginalActionCapability.perform)
|
||||||
|
.toHaveBeenCalledWith('navigate');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -1,3 +1,25 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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,jasmine */
|
/*global define,describe,it,expect,beforeEach,jasmine */
|
||||||
|
|
||||||
define(
|
define(
|
||||||
@ -7,6 +29,7 @@ define(
|
|||||||
'../ControlledPromise'
|
'../ControlledPromise'
|
||||||
],
|
],
|
||||||
function (LocationCapability, domainObjectFactory, ControlledPromise) {
|
function (LocationCapability, domainObjectFactory, ControlledPromise) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
describe("LocationCapability", function () {
|
describe("LocationCapability", function () {
|
||||||
|
|
||||||
@ -14,13 +37,17 @@ define(
|
|||||||
var locationCapability,
|
var locationCapability,
|
||||||
persistencePromise,
|
persistencePromise,
|
||||||
mutationPromise,
|
mutationPromise,
|
||||||
|
mockQ,
|
||||||
|
mockInjector,
|
||||||
|
mockObjectService,
|
||||||
domainObject;
|
domainObject;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
domainObject = domainObjectFactory({
|
domainObject = domainObjectFactory({
|
||||||
|
id: "testObject",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
context: {
|
context: {
|
||||||
getParent: function() {
|
getParent: function () {
|
||||||
return domainObjectFactory({id: 'root'});
|
return domainObjectFactory({id: 'root'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -35,6 +62,11 @@ define(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mockQ = jasmine.createSpyObj("$q", ["when"]);
|
||||||
|
mockInjector = jasmine.createSpyObj("$injector", ["get"]);
|
||||||
|
mockObjectService =
|
||||||
|
jasmine.createSpyObj("objectService", ["getObjects"]);
|
||||||
|
|
||||||
persistencePromise = new ControlledPromise();
|
persistencePromise = new ControlledPromise();
|
||||||
domainObject.capabilities.persistence.persist.andReturn(
|
domainObject.capabilities.persistence.persist.andReturn(
|
||||||
persistencePromise
|
persistencePromise
|
||||||
@ -49,7 +81,11 @@ define(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
locationCapability = new LocationCapability(domainObject);
|
locationCapability = new LocationCapability(
|
||||||
|
mockQ,
|
||||||
|
mockInjector,
|
||||||
|
domainObject
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns contextual location", function () {
|
it("returns contextual location", function () {
|
||||||
@ -88,6 +124,57 @@ define(
|
|||||||
expect(whenComplete).toHaveBeenCalled();
|
expect(whenComplete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when used to load an original instance", function () {
|
||||||
|
var objectPromise,
|
||||||
|
qPromise,
|
||||||
|
originalObjects,
|
||||||
|
mockCallback;
|
||||||
|
|
||||||
|
function resolvePromises() {
|
||||||
|
if (mockQ.when.calls.length > 0) {
|
||||||
|
qPromise.resolve(mockQ.when.mostRecentCall.args[0]);
|
||||||
|
}
|
||||||
|
if (mockObjectService.getObjects.calls.length > 0) {
|
||||||
|
objectPromise.resolve(originalObjects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
objectPromise = new ControlledPromise();
|
||||||
|
qPromise = new ControlledPromise();
|
||||||
|
originalObjects = {
|
||||||
|
testObject: domainObjectFactory()
|
||||||
|
};
|
||||||
|
|
||||||
|
mockInjector.get.andCallFake(function (key) {
|
||||||
|
return key === 'objectService' && mockObjectService;
|
||||||
|
});
|
||||||
|
mockObjectService.getObjects.andReturn(objectPromise);
|
||||||
|
mockQ.when.andReturn(qPromise);
|
||||||
|
|
||||||
|
mockCallback = jasmine.createSpy('callback');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides originals directly", function () {
|
||||||
|
domainObject.model.location = 'root';
|
||||||
|
locationCapability.getOriginal().then(mockCallback);
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
resolvePromises();
|
||||||
|
expect(mockCallback)
|
||||||
|
.toHaveBeenCalledWith(domainObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads from the object service for links", function () {
|
||||||
|
domainObject.model.location = 'some-other-root';
|
||||||
|
locationCapability.getOriginal().then(mockCallback);
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
resolvePromises();
|
||||||
|
expect(mockCallback)
|
||||||
|
.toHaveBeenCalledWith(originalObjects.testObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
[
|
[
|
||||||
"actions/AbstractComposeAction",
|
"actions/AbstractComposeAction",
|
||||||
|
"actions/CopyAction",
|
||||||
|
"actions/GoToOriginalAction",
|
||||||
|
"actions/LinkAction",
|
||||||
|
"actions/MoveAction",
|
||||||
"services/CopyService",
|
"services/CopyService",
|
||||||
"services/LinkService",
|
"services/LinkService",
|
||||||
"services/MoveService",
|
"services/MoveService",
|
||||||
|
@ -159,7 +159,9 @@ define(
|
|||||||
|
|
||||||
// Update dimensions and origin based on extrema of plots
|
// Update dimensions and origin based on extrema of plots
|
||||||
PlotUpdater.prototype.updateBounds = function () {
|
PlotUpdater.prototype.updateBounds = function () {
|
||||||
var bufferArray = this.bufferArray,
|
var bufferArray = this.bufferArray.filter(function (lineBuffer) {
|
||||||
|
return lineBuffer.getLength() > 0; // Ignore empty lines
|
||||||
|
}),
|
||||||
priorDomainOrigin = this.origin[0],
|
priorDomainOrigin = this.origin[0],
|
||||||
priorDomainDimensions = this.dimensions[0];
|
priorDomainDimensions = this.dimensions[0];
|
||||||
|
|
||||||
|
@ -202,6 +202,38 @@ define(
|
|||||||
expect(updater.getDimensions()[1]).toBeGreaterThan(20);
|
expect(updater.getDimensions()[1]).toBeGreaterThan(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when no data is initially available", function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
testDomainValues = {};
|
||||||
|
testRangeValues = {};
|
||||||
|
updater = new PlotUpdater(
|
||||||
|
mockSubscription,
|
||||||
|
testDomain,
|
||||||
|
testRange,
|
||||||
|
1350 // Smaller max size for easier testing
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no line data", function () {
|
||||||
|
// Either no lines, or empty lines are fine
|
||||||
|
expect(updater.getLineBuffers().map(function (lineBuffer) {
|
||||||
|
return lineBuffer.getLength();
|
||||||
|
}).reduce(function (a, b) {
|
||||||
|
return a + b;
|
||||||
|
}, 0)).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("determines initial domain bounds from first available data", function () {
|
||||||
|
testDomainValues.a = 123;
|
||||||
|
testRangeValues.a = 456;
|
||||||
|
updater.update();
|
||||||
|
expect(updater.getOrigin()[0]).toEqual(jasmine.any(Number));
|
||||||
|
expect(updater.getOrigin()[1]).toEqual(jasmine.any(Number));
|
||||||
|
expect(isNaN(updater.getOrigin()[0])).toBeFalsy();
|
||||||
|
expect(isNaN(updater.getOrigin()[1])).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"provides": "searchService",
|
"provides": "searchService",
|
||||||
"type": "provider",
|
"type": "provider",
|
||||||
"implementation": "ElasticSearchProvider.js",
|
"implementation": "ElasticSearchProvider.js",
|
||||||
"depends": [ "$http", "objectService", "ELASTIC_ROOT" ]
|
"depends": [ "$http", "ELASTIC_ROOT" ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"constants": [
|
"constants": [
|
||||||
|
@ -24,190 +24,122 @@
|
|||||||
/**
|
/**
|
||||||
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
|
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// JSLint doesn't like underscore-prefixed properties,
|
], function (
|
||||||
// so hide them here.
|
|
||||||
var ID = "_id",
|
|
||||||
SCORE = "_score",
|
|
||||||
DEFAULT_MAX_RESULTS = 100;
|
|
||||||
|
|
||||||
/**
|
) {
|
||||||
* A search service which searches through domain objects in
|
"use strict";
|
||||||
* the filetree using ElasticSearch.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param $http Angular's $http service, for working with urls.
|
|
||||||
* @param {ObjectService} objectService the service from which
|
|
||||||
* domain objects can be gotten.
|
|
||||||
* @param ROOT the constant `ELASTIC_ROOT` which allows us to
|
|
||||||
* interact with ElasticSearch.
|
|
||||||
*/
|
|
||||||
function ElasticSearchProvider($http, objectService, ROOT) {
|
|
||||||
this.$http = $http;
|
|
||||||
this.objectService = objectService;
|
|
||||||
this.root = ROOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
var ID_PROPERTY = '_id',
|
||||||
* Searches through the filetree for domain objects using a search
|
SOURCE_PROPERTY = '_source',
|
||||||
* term. This is done through querying elasticsearch. Returns a
|
SCORE_PROPERTY = '_score';
|
||||||
* promise for a result object that has the format
|
|
||||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
|
||||||
* where a searchResult has the format
|
|
||||||
* {id: string, object: domainObject, score: number}
|
|
||||||
*
|
|
||||||
* Notes:
|
|
||||||
* * The order of the results is from highest to lowest score,
|
|
||||||
* as elsaticsearch determines them to be.
|
|
||||||
* * Uses the fuzziness operator to get more results.
|
|
||||||
* * More about this search's behavior at
|
|
||||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html
|
|
||||||
*
|
|
||||||
* @param searchTerm The text input that is the query.
|
|
||||||
* @param timestamp The time at which this function was called.
|
|
||||||
* This timestamp is used as a unique identifier for this
|
|
||||||
* query and the corresponding results.
|
|
||||||
* @param maxResults (optional) The maximum number of results
|
|
||||||
* that this function should return.
|
|
||||||
* @param timeout (optional) The time after which the search should
|
|
||||||
* stop calculations and return partial results. Elasticsearch
|
|
||||||
* does not guarentee that this timeout will be strictly followed.
|
|
||||||
*/
|
|
||||||
ElasticSearchProvider.prototype.query = function query(searchTerm, timestamp, maxResults, timeout) {
|
|
||||||
var $http = this.$http,
|
|
||||||
objectService = this.objectService,
|
|
||||||
root = this.root,
|
|
||||||
esQuery;
|
|
||||||
|
|
||||||
function addFuzziness(searchTerm, editDistance) {
|
/**
|
||||||
if (!editDistance) {
|
* A search service which searches through domain objects in
|
||||||
editDistance = '';
|
* the filetree using ElasticSearch.
|
||||||
}
|
*
|
||||||
|
* @constructor
|
||||||
return searchTerm.split(' ').map(function (s) {
|
* @param $http Angular's $http service, for working with urls.
|
||||||
// Don't add fuzziness for quoted strings
|
* @param ROOT the constant `ELASTIC_ROOT` which allows us to
|
||||||
if (s.indexOf('"') !== -1) {
|
* interact with ElasticSearch.
|
||||||
return s;
|
*/
|
||||||
} else {
|
function ElasticSearchProvider($http, ROOT) {
|
||||||
return s + '~' + editDistance;
|
this.$http = $http;
|
||||||
}
|
this.root = ROOT;
|
||||||
}).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently specific to elasticsearch
|
|
||||||
function processSearchTerm(searchTerm) {
|
|
||||||
var spaceIndex;
|
|
||||||
|
|
||||||
// Cut out any extra spaces
|
|
||||||
while (searchTerm.substr(0, 1) === ' ') {
|
|
||||||
searchTerm = searchTerm.substring(1, searchTerm.length);
|
|
||||||
}
|
|
||||||
while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') {
|
|
||||||
searchTerm = searchTerm.substring(0, searchTerm.length - 1);
|
|
||||||
}
|
|
||||||
spaceIndex = searchTerm.indexOf(' ');
|
|
||||||
while (spaceIndex !== -1) {
|
|
||||||
searchTerm = searchTerm.substring(0, spaceIndex) +
|
|
||||||
searchTerm.substring(spaceIndex + 1, searchTerm.length);
|
|
||||||
spaceIndex = searchTerm.indexOf(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add fuzziness for completeness
|
|
||||||
searchTerm = addFuzziness(searchTerm);
|
|
||||||
|
|
||||||
return searchTerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processes results from the format that elasticsearch returns to
|
|
||||||
// a list of searchResult objects, then returns a result object
|
|
||||||
// (See documentation for query for object descriptions)
|
|
||||||
function processResults(rawResults, timestamp) {
|
|
||||||
var results = rawResults.data.hits.hits,
|
|
||||||
resultsLength = results.length,
|
|
||||||
ids = [],
|
|
||||||
scores = {},
|
|
||||||
searchResults = [],
|
|
||||||
i;
|
|
||||||
|
|
||||||
// Get the result objects' IDs
|
|
||||||
for (i = 0; i < resultsLength; i += 1) {
|
|
||||||
ids.push(results[i][ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the result objects' scores
|
|
||||||
for (i = 0; i < resultsLength; i += 1) {
|
|
||||||
scores[ids[i]] = results[i][SCORE];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the domain objects from their IDs
|
|
||||||
return objectService.getObjects(ids).then(function (objects) {
|
|
||||||
var j,
|
|
||||||
id;
|
|
||||||
|
|
||||||
for (j = 0; j < resultsLength; j += 1) {
|
|
||||||
id = ids[j];
|
|
||||||
|
|
||||||
// Include items we can get models for
|
|
||||||
if (objects[id].getModel) {
|
|
||||||
// Format the results as searchResult objects
|
|
||||||
searchResults.push({
|
|
||||||
id: id,
|
|
||||||
object: objects[id],
|
|
||||||
score: scores[id]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hits: searchResults,
|
|
||||||
total: rawResults.data.hits.total,
|
|
||||||
timedOut: rawResults.data.timed_out
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Check to see if the user provided a maximum
|
|
||||||
// number of results to display
|
|
||||||
if (!maxResults) {
|
|
||||||
// Else, we provide a default value.
|
|
||||||
maxResults = DEFAULT_MAX_RESULTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user input is empty, we want to have no search results.
|
|
||||||
if (searchTerm !== '' && searchTerm !== undefined) {
|
|
||||||
// Process the search term
|
|
||||||
searchTerm = processSearchTerm(searchTerm);
|
|
||||||
|
|
||||||
// Create the query to elasticsearch
|
|
||||||
esQuery = root + "/_search/?q=" + searchTerm +
|
|
||||||
"&size=" + maxResults;
|
|
||||||
if (timeout) {
|
|
||||||
esQuery += "&timeout=" + timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the data...
|
|
||||||
return this.$http({
|
|
||||||
method: "GET",
|
|
||||||
url: esQuery
|
|
||||||
}).then(function (rawResults) {
|
|
||||||
// ...then process the data
|
|
||||||
return processResults(rawResults, timestamp);
|
|
||||||
}, function (err) {
|
|
||||||
// In case of error, return nothing. (To prevent
|
|
||||||
// infinite loading time.)
|
|
||||||
return {hits: [], total: 0};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return {hits: [], total: 0};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return ElasticSearchProvider;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* Search for domain objects using elasticsearch as a search provider.
|
||||||
|
*
|
||||||
|
* @param {String} searchTerm the term to search by.
|
||||||
|
* @param {Number} [maxResults] the max numer of results to return.
|
||||||
|
* @returns {Promise} promise for a modelResults object.
|
||||||
|
*/
|
||||||
|
ElasticSearchProvider.prototype.query = function (searchTerm, maxResults) {
|
||||||
|
var searchUrl = this.root + '/_search/',
|
||||||
|
params = {},
|
||||||
|
provider = this;
|
||||||
|
|
||||||
|
searchTerm = this.cleanTerm(searchTerm);
|
||||||
|
searchTerm = this.fuzzyMatchUnquotedTerms(searchTerm);
|
||||||
|
|
||||||
|
params.q = searchTerm;
|
||||||
|
params.size = maxResults;
|
||||||
|
|
||||||
|
return this
|
||||||
|
.$http({
|
||||||
|
method: "GET",
|
||||||
|
url: searchUrl,
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
.then(function success(succesResponse) {
|
||||||
|
return provider.parseResponse(succesResponse);
|
||||||
|
}, function error(errorResponse) {
|
||||||
|
// Gracefully fail.
|
||||||
|
return {
|
||||||
|
hits: [],
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean excess whitespace from a search term and return the cleaned
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} the search term to clean.
|
||||||
|
* @returns {string} search terms cleaned of excess whitespace.
|
||||||
|
*/
|
||||||
|
ElasticSearchProvider.prototype.cleanTerm = function (term) {
|
||||||
|
return term.trim().replace(/ +/g, ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add fuzzy matching markup to search terms that are not quoted.
|
||||||
|
*
|
||||||
|
* The following:
|
||||||
|
* hello welcome "to quoted village" have fun
|
||||||
|
* will become
|
||||||
|
* hello~ welcome~ "to quoted village" have~ fun~
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ElasticSearchProvider.prototype.fuzzyMatchUnquotedTerms = function (query) {
|
||||||
|
var matchUnquotedSpaces = '\\s+(?=([^"]*"[^"]*")*[^"]*$)',
|
||||||
|
matcher = new RegExp(matchUnquotedSpaces, 'g');
|
||||||
|
|
||||||
|
return query
|
||||||
|
.replace(matcher, '~ ')
|
||||||
|
.replace(/$/, '~')
|
||||||
|
.replace(/"~+/, '"');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from ElasticSearch and convert it to a
|
||||||
|
* modelResults object.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param response a ES response object from $http
|
||||||
|
* @returns modelResults
|
||||||
|
*/
|
||||||
|
ElasticSearchProvider.prototype.parseResponse = function (response) {
|
||||||
|
var results = response.data.hits.hits,
|
||||||
|
searchResults = results.map(function (result) {
|
||||||
|
return {
|
||||||
|
id: result[ID_PROPERTY],
|
||||||
|
model: result[SOURCE_PROPERTY],
|
||||||
|
score: result[SCORE_PROPERTY]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hits: searchResults,
|
||||||
|
total: response.data.hits.total
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return ElasticSearchProvider;
|
||||||
|
});
|
||||||
|
@ -19,97 +19,151 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
/*global define,describe,it,expect,beforeEach,jasmine,spyOn,Promise,waitsFor*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchSpec. Created by shale on 07/31/2015.
|
* SearchSpec. Created by shale on 07/31/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
["../src/ElasticSearchProvider"],
|
'../src/ElasticSearchProvider'
|
||||||
function (ElasticSearchProvider) {
|
], function (
|
||||||
"use strict";
|
ElasticSearchProvider
|
||||||
|
) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// JSLint doesn't like underscore-prefixed properties,
|
describe('ElasticSearchProvider', function () {
|
||||||
// so hide them here.
|
var $http,
|
||||||
var ID = "_id",
|
ROOT,
|
||||||
SCORE = "_score";
|
provider;
|
||||||
|
|
||||||
describe("The ElasticSearch search provider ", function () {
|
beforeEach(function () {
|
||||||
var mockHttp,
|
$http = jasmine.createSpy('$http');
|
||||||
mockHttpPromise,
|
ROOT = 'http://localhost:9200';
|
||||||
mockObjectPromise,
|
|
||||||
mockObjectService,
|
|
||||||
mockDomainObject,
|
|
||||||
provider,
|
|
||||||
mockProviderResults;
|
|
||||||
|
|
||||||
|
provider = new ElasticSearchProvider($http, ROOT);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockHttp = jasmine.createSpy("$http");
|
spyOn(provider, 'cleanTerm').andReturn('cleanedTerm');
|
||||||
mockHttpPromise = jasmine.createSpyObj(
|
spyOn(provider, 'fuzzyMatchUnquotedTerms').andReturn('fuzzy');
|
||||||
"promise",
|
spyOn(provider, 'parseResponse').andReturn('parsedResponse');
|
||||||
[ "then" ]
|
$http.andReturn(Promise.resolve({}));
|
||||||
);
|
|
||||||
mockHttp.andReturn(mockHttpPromise);
|
|
||||||
// allow chaining of promise.then().catch();
|
|
||||||
mockHttpPromise.then.andReturn(mockHttpPromise);
|
|
||||||
|
|
||||||
mockObjectService = jasmine.createSpyObj(
|
|
||||||
"objectService",
|
|
||||||
[ "getObjects" ]
|
|
||||||
);
|
|
||||||
mockObjectPromise = jasmine.createSpyObj(
|
|
||||||
"promise",
|
|
||||||
[ "then" ]
|
|
||||||
);
|
|
||||||
mockObjectService.getObjects.andReturn(mockObjectPromise);
|
|
||||||
|
|
||||||
mockDomainObject = jasmine.createSpyObj(
|
|
||||||
"domainObject",
|
|
||||||
[ "getId", "getModel" ]
|
|
||||||
);
|
|
||||||
|
|
||||||
provider = new ElasticSearchProvider(mockHttp, mockObjectService, "");
|
|
||||||
provider.query(' test "query" ', 0, undefined, 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends a query to ElasticSearch", function () {
|
it('cleans terms and adds fuzzyness', function () {
|
||||||
expect(mockHttp).toHaveBeenCalled();
|
provider.query('hello', 10);
|
||||||
|
expect(provider.cleanTerm).toHaveBeenCalledWith('hello');
|
||||||
|
expect(provider.fuzzyMatchUnquotedTerms)
|
||||||
|
.toHaveBeenCalledWith('cleanedTerm');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets data from ElasticSearch", function () {
|
it('calls through to $http', function () {
|
||||||
var data = {
|
provider.query('hello', 10);
|
||||||
hits: {
|
expect($http).toHaveBeenCalledWith({
|
||||||
hits: [
|
method: 'GET',
|
||||||
{},
|
params: {
|
||||||
{}
|
q: 'fuzzy',
|
||||||
],
|
size: 10
|
||||||
total: 0
|
|
||||||
},
|
},
|
||||||
timed_out: false
|
url: 'http://localhost:9200/_search/'
|
||||||
};
|
});
|
||||||
data.hits.hits[0][ID] = 1;
|
|
||||||
data.hits.hits[0][SCORE] = 1;
|
|
||||||
data.hits.hits[1][ID] = 2;
|
|
||||||
data.hits.hits[1][SCORE] = 2;
|
|
||||||
|
|
||||||
mockProviderResults = mockHttpPromise.then.mostRecentCall.args[0]({data: data});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
mockObjectPromise.then.mostRecentCall.args[0]({
|
|
||||||
1: mockDomainObject,
|
|
||||||
2: mockDomainObject
|
|
||||||
}).hits.length
|
|
||||||
).toEqual(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns nothing for an empty string query", function () {
|
it('gracefully fails when http fails', function () {
|
||||||
expect(provider.query("").hits).toEqual([]);
|
var promiseChainResolved = false;
|
||||||
|
$http.andReturn(Promise.reject());
|
||||||
|
|
||||||
|
provider
|
||||||
|
.query('hello', 10)
|
||||||
|
.then(function (results) {
|
||||||
|
expect(results).toEqual({
|
||||||
|
hits: [],
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
promiseChainResolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
waitsFor(function () {
|
||||||
|
return promiseChainResolved;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns something when there is an ElasticSearch error", function () {
|
it('parses and returns when http succeeds', function () {
|
||||||
mockProviderResults = mockHttpPromise.then.mostRecentCall.args[1]();
|
var promiseChainResolved = false;
|
||||||
expect(mockProviderResults).toBeDefined();
|
$http.andReturn(Promise.resolve('successResponse'));
|
||||||
|
|
||||||
|
provider
|
||||||
|
.query('hello', 10)
|
||||||
|
.then(function (results) {
|
||||||
|
expect(provider.parseResponse)
|
||||||
|
.toHaveBeenCalledWith('successResponse');
|
||||||
|
expect(results).toBe('parsedResponse');
|
||||||
|
promiseChainResolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
waitsFor(function () {
|
||||||
|
return promiseChainResolved;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
it('can clean terms', function () {
|
||||||
|
expect(provider.cleanTerm(' asdhs ')).toBe('asdhs');
|
||||||
|
expect(provider.cleanTerm(' and some words'))
|
||||||
|
.toBe('and some words');
|
||||||
|
expect(provider.cleanTerm('Nice input')).toBe('Nice input');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create fuzzy term matchers', function () {
|
||||||
|
expect(provider.fuzzyMatchUnquotedTerms('pwr dvc 43'))
|
||||||
|
.toBe('pwr~ dvc~ 43~');
|
||||||
|
|
||||||
|
expect(provider.fuzzyMatchUnquotedTerms(
|
||||||
|
'hello welcome "to quoted village" have fun'
|
||||||
|
)).toBe(
|
||||||
|
'hello~ welcome~ "to quoted village" have~ fun~'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can parse responses', function () {
|
||||||
|
var elasticSearchResponse = {
|
||||||
|
data: {
|
||||||
|
hits: {
|
||||||
|
total: 2,
|
||||||
|
hits: [
|
||||||
|
{
|
||||||
|
'_id': 'hit1Id',
|
||||||
|
'_source': 'hit1Model',
|
||||||
|
'_score': 0.56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'_id': 'hit2Id',
|
||||||
|
'_source': 'hit2Model',
|
||||||
|
'_score': 0.34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(provider.parseResponse(elasticSearchResponse))
|
||||||
|
.toEqual({
|
||||||
|
hits: [
|
||||||
|
{
|
||||||
|
id: 'hit1Id',
|
||||||
|
model: 'hit1Model',
|
||||||
|
score: 0.56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hit2Id',
|
||||||
|
model: 'hit2Model',
|
||||||
|
score: 0.34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
@ -48,8 +48,7 @@
|
|||||||
"depends": [
|
"depends": [
|
||||||
"$q",
|
"$q",
|
||||||
"$log",
|
"$log",
|
||||||
"throttle",
|
"modelService",
|
||||||
"objectService",
|
|
||||||
"workerService",
|
"workerService",
|
||||||
"topic",
|
"topic",
|
||||||
"GENERIC_SEARCH_ROOTS"
|
"GENERIC_SEARCH_ROOTS"
|
||||||
@ -59,7 +58,7 @@
|
|||||||
"provides": "searchService",
|
"provides": "searchService",
|
||||||
"type": "aggregator",
|
"type": "aggregator",
|
||||||
"implementation": "services/SearchAggregator.js",
|
"implementation": "services/SearchAggregator.js",
|
||||||
"depends": [ "$q" ]
|
"depends": [ "$q", "objectService" ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"workers": [
|
"workers": [
|
||||||
|
@ -31,11 +31,6 @@
|
|||||||
type="text"
|
type="text"
|
||||||
ng-model="ngModel.input"
|
ng-model="ngModel.input"
|
||||||
ng-keyup="controller.search()" />
|
ng-keyup="controller.search()" />
|
||||||
<!--mct-control key="'textfield'"
|
|
||||||
class="search-input"
|
|
||||||
ng-model="ngModel.input"
|
|
||||||
ng-keyup="controller.search()">
|
|
||||||
</mct-control-->
|
|
||||||
|
|
||||||
<!-- Search icon -->
|
<!-- Search icon -->
|
||||||
<!-- ui symbols for search are 'd' and 'M' -->
|
<!-- ui symbols for search are 'd' and 'M' -->
|
||||||
@ -78,9 +73,6 @@
|
|||||||
|
|
||||||
Filtered by: {{ ngModel.filtersString }}
|
Filtered by: {{ ngModel.filtersString }}
|
||||||
|
|
||||||
<!--div class="filter-options">
|
|
||||||
Filtered by: {{ ngModel.filtersString }}
|
|
||||||
</div-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- This div exists to determine scroll bar location -->
|
<!-- This div exists to determine scroll bar location -->
|
||||||
|
@ -27,145 +27,154 @@
|
|||||||
define(function () {
|
define(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var INITIAL_LOAD_NUMBER = 20,
|
/**
|
||||||
LOAD_INCREMENT = 20;
|
* Controller for search in Tree View.
|
||||||
|
*
|
||||||
|
* Filtering is currently buggy; it filters after receiving results from
|
||||||
|
* search providers, the downside of this is that it requires search
|
||||||
|
* providers to provide objects for all possible results, which is
|
||||||
|
* potentially a hit to persistence, thus can be very very slow.
|
||||||
|
*
|
||||||
|
* Ideally, filtering should be handled before loading objects from the persistence
|
||||||
|
* store, the downside to this is that filters must be applied to object
|
||||||
|
* models, not object instances.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param $scope
|
||||||
|
* @param searchService
|
||||||
|
*/
|
||||||
function SearchController($scope, searchService) {
|
function SearchController($scope, searchService) {
|
||||||
// numResults is the amount of results to display. Will get increased.
|
var controller = this;
|
||||||
// fullResults holds the most recent complete searchService response object
|
this.$scope = $scope;
|
||||||
var numResults = INITIAL_LOAD_NUMBER,
|
this.searchService = searchService;
|
||||||
fullResults = {hits: []};
|
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||||
|
this.availabileResults = 0;
|
||||||
// Scope variables are:
|
this.$scope.results = [];
|
||||||
// Variables used only in SearchController:
|
this.$scope.loading = false;
|
||||||
// results, an array of searchResult objects
|
this.pendingQuery = undefined;
|
||||||
// loading, whether search() is loading
|
this.$scope.ngModel.filter = function () {
|
||||||
// ngModel.input, the text of the search query
|
return controller.onFilterChange.apply(controller, arguments);
|
||||||
// ngModel.search, a boolean of whether to display search or the tree
|
|
||||||
// Variables used also in SearchMenuController:
|
|
||||||
// ngModel.filter, the function filter defined below
|
|
||||||
// ngModel.types, an array of type objects
|
|
||||||
// ngModel.checked, a dictionary of which type filter options are checked
|
|
||||||
// ngModel.checkAll, a boolean of whether to search all types
|
|
||||||
// ngModel.filtersString, a string list of what filters on the results are active
|
|
||||||
$scope.results = [];
|
|
||||||
$scope.loading = false;
|
|
||||||
|
|
||||||
|
|
||||||
// Filters searchResult objects by type. Allows types that are
|
|
||||||
// checked. (ngModel.checked['typekey'] === true)
|
|
||||||
// If hits is not provided, will use fullResults.hits
|
|
||||||
function filter(hits) {
|
|
||||||
var newResults = [],
|
|
||||||
i = 0;
|
|
||||||
|
|
||||||
if (!hits) {
|
|
||||||
hits = fullResults.hits;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If checkAll is checked, search everything no matter what the other
|
|
||||||
// checkboxes' statuses are. Otherwise filter the search by types.
|
|
||||||
if ($scope.ngModel.checkAll) {
|
|
||||||
newResults = fullResults.hits.slice(0, numResults);
|
|
||||||
} else {
|
|
||||||
while (newResults.length < numResults && i < hits.length) {
|
|
||||||
// If this is of an acceptable type, add it to the list
|
|
||||||
if ($scope.ngModel.checked[hits[i].object.getModel().type]) {
|
|
||||||
newResults.push(fullResults.hits[i]);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.results = newResults;
|
|
||||||
return newResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make function accessible from SearchMenuController
|
|
||||||
$scope.ngModel.filter = filter;
|
|
||||||
|
|
||||||
// For documentation, see search below
|
|
||||||
function search(maxResults) {
|
|
||||||
var inputText = $scope.ngModel.input;
|
|
||||||
|
|
||||||
if (inputText !== '' && inputText !== undefined) {
|
|
||||||
// We are starting to load.
|
|
||||||
$scope.loading = true;
|
|
||||||
|
|
||||||
// Update whether the file tree should be displayed
|
|
||||||
// Hide tree only when starting search
|
|
||||||
$scope.ngModel.search = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!maxResults) {
|
|
||||||
// Reset 'load more'
|
|
||||||
numResults = INITIAL_LOAD_NUMBER;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the query
|
|
||||||
searchService.query(inputText, maxResults).then(function (result) {
|
|
||||||
// Store all the results before splicing off the front, so that
|
|
||||||
// we can load more to display later.
|
|
||||||
fullResults = result;
|
|
||||||
$scope.results = filter(result.hits);
|
|
||||||
|
|
||||||
// Update whether the file tree should be displayed
|
|
||||||
// Reveal tree only when finishing search
|
|
||||||
if (inputText === '' || inputText === undefined) {
|
|
||||||
$scope.ngModel.search = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we are done loading.
|
|
||||||
$scope.loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Search the filetree. Assumes that any search text will
|
|
||||||
* be in ngModel.input
|
|
||||||
*
|
|
||||||
* @param maxResults (optional) The maximum number of results
|
|
||||||
* that this function should return. If not provided, search
|
|
||||||
* service default will be used.
|
|
||||||
*/
|
|
||||||
search: search,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks to see if there are more search results to display. If the answer is
|
|
||||||
* unclear, this function will err toward saying that there are more results.
|
|
||||||
*/
|
|
||||||
areMore: function () {
|
|
||||||
var i;
|
|
||||||
|
|
||||||
// Check to see if any of the not displayed results are of an allowed type
|
|
||||||
for (i = numResults; i < fullResults.hits.length; i += 1) {
|
|
||||||
if ($scope.ngModel.checkAll || $scope.ngModel.checked[fullResults.hits[i].object.getModel().type]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If none of the ones at hand are correct, there still may be more if we
|
|
||||||
// re-search with a larger maxResults
|
|
||||||
return fullResults.hits.length < fullResults.total;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increases the number of search results to display, and then
|
|
||||||
* loads them, adding to the displayed results.
|
|
||||||
*/
|
|
||||||
loadMore: function () {
|
|
||||||
numResults += LOAD_INCREMENT;
|
|
||||||
|
|
||||||
if (numResults > fullResults.hits.length && fullResults.hits.length < fullResults.total) {
|
|
||||||
// Resend the query if we are out of items to display, but there are more to get
|
|
||||||
search(numResults);
|
|
||||||
} else {
|
|
||||||
// Otherwise just take from what we already have
|
|
||||||
$scope.results = filter(fullResults.hits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SearchController.prototype.RESULTS_PER_PAGE = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there are more results than currently displayed for the
|
||||||
|
* for the current query and filters.
|
||||||
|
*/
|
||||||
|
SearchController.prototype.areMore = function () {
|
||||||
|
return this.$scope.results.length < this.availableResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display more results for the currently displayed query and filters.
|
||||||
|
*/
|
||||||
|
SearchController.prototype.loadMore = function () {
|
||||||
|
this.numberToDisplay += this.RESULTS_PER_PAGE;
|
||||||
|
this.dispatchSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset search results, then search for the query string specified in
|
||||||
|
* scope.
|
||||||
|
*/
|
||||||
|
SearchController.prototype.search = function () {
|
||||||
|
var inputText = this.$scope.ngModel.input;
|
||||||
|
|
||||||
|
this.clearResults();
|
||||||
|
|
||||||
|
if (inputText) {
|
||||||
|
this.$scope.loading = true;
|
||||||
|
this.$scope.ngModel.search = true;
|
||||||
|
} else {
|
||||||
|
this.pendingQuery = undefined;
|
||||||
|
this.$scope.ngModel.search = false;
|
||||||
|
this.$scope.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a search to the search service if it hasn't already been
|
||||||
|
* dispatched.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
SearchController.prototype.dispatchSearch = function () {
|
||||||
|
var inputText = this.$scope.ngModel.input,
|
||||||
|
controller = this,
|
||||||
|
queryId = inputText + this.numberToDisplay;
|
||||||
|
|
||||||
|
if (this.pendingQuery === queryId) {
|
||||||
|
return; // don't issue multiple queries for the same term.
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingQuery = queryId;
|
||||||
|
|
||||||
|
this
|
||||||
|
.searchService
|
||||||
|
.query(inputText, this.numberToDisplay, this.filterPredicate())
|
||||||
|
.then(function (results) {
|
||||||
|
if (controller.pendingQuery !== queryId) {
|
||||||
|
return; // another query in progress, so skip this one.
|
||||||
|
}
|
||||||
|
controller.onSearchComplete(results);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchController.prototype.filter = SearchController.prototype.onFilterChange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refilter results and update visible results when filters have changed.
|
||||||
|
*/
|
||||||
|
SearchController.prototype.onFilterChange = function () {
|
||||||
|
this.pendingQuery = undefined;
|
||||||
|
this.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a predicate function that can be used to filter object models.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
SearchController.prototype.filterPredicate = function () {
|
||||||
|
if (this.$scope.ngModel.checkAll) {
|
||||||
|
return function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var includeTypes = this.$scope.ngModel.checked;
|
||||||
|
return function (model) {
|
||||||
|
return !!includeTypes[model.type];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the search results.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
SearchController.prototype.clearResults = function () {
|
||||||
|
this.$scope.results = [];
|
||||||
|
this.availableResults = 0;
|
||||||
|
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update search results from given `results`.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
SearchController.prototype.onSearchComplete = function (results) {
|
||||||
|
this.availableResults = results.total;
|
||||||
|
this.$scope.results = results.hits;
|
||||||
|
this.$scope.loading = false;
|
||||||
|
this.pendingQuery = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return SearchController;
|
return SearchController;
|
||||||
});
|
});
|
||||||
|
@ -19,234 +19,262 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
/*global define*/
|
/*global define,setTimeout*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
|
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var DEFAULT_MAX_RESULTS = 100,
|
], function (
|
||||||
DEFAULT_TIMEOUT = 1000,
|
|
||||||
MAX_CONCURRENT_REQUESTS = 100,
|
|
||||||
FLUSH_INTERVAL = 0,
|
|
||||||
stopTime;
|
|
||||||
|
|
||||||
/**
|
) {
|
||||||
* A search service which searches through domain objects in
|
"use strict";
|
||||||
* the filetree without using external search implementations.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param $q Angular's $q, for promise consolidation.
|
|
||||||
* @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
|
|
||||||
* domain objects' IDs.
|
|
||||||
*/
|
|
||||||
function GenericSearchProvider($q, $log, throttle, objectService, workerService, topic, ROOTS) {
|
|
||||||
var indexed = {},
|
|
||||||
pendingIndex = {},
|
|
||||||
pendingQueries = {},
|
|
||||||
toRequest = [],
|
|
||||||
worker = workerService.run('genericSearchWorker'),
|
|
||||||
mutationTopic = topic("mutation"),
|
|
||||||
indexingStarted = Date.now(),
|
|
||||||
pendingRequests = 0,
|
|
||||||
scheduleFlush;
|
|
||||||
|
|
||||||
this.worker = worker;
|
/**
|
||||||
this.pendingQueries = pendingQueries;
|
* A search service which searches through domain objects in
|
||||||
this.$q = $q;
|
* the filetree without using external search implementations.
|
||||||
// pendingQueries is a dictionary with the key value pairs st
|
*
|
||||||
// the key is the timestamp and the value is the promise
|
* @constructor
|
||||||
|
* @param $q Angular's $q, for promise consolidation.
|
||||||
|
* @param $log Anglar's $log, for logging.
|
||||||
|
* @param {ModelService} modelService the model service.
|
||||||
|
* @param {WorkerService} workerService the workerService.
|
||||||
|
* @param {TopicService} topic the topic service.
|
||||||
|
* @param {Array} ROOTS An array of object Ids to begin indexing.
|
||||||
|
*/
|
||||||
|
function GenericSearchProvider($q, $log, modelService, workerService, topic, ROOTS) {
|
||||||
|
var provider = this;
|
||||||
|
this.$q = $q;
|
||||||
|
this.$log = $log;
|
||||||
|
this.modelService = modelService;
|
||||||
|
|
||||||
function scheduleIdsForIndexing(ids) {
|
this.indexedIds = {};
|
||||||
ids.forEach(function (id) {
|
this.idsToIndex = [];
|
||||||
if (!indexed[id] && !pendingIndex[id]) {
|
this.pendingIndex = {};
|
||||||
indexed[id] = true;
|
this.pendingRequests = 0;
|
||||||
pendingIndex[id] = true;
|
|
||||||
toRequest.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scheduleFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the web worker to add a domain object's model to its list of items.
|
this.pendingQueries = {};
|
||||||
function indexItem(domainObject) {
|
|
||||||
var model = domainObject.getModel();
|
|
||||||
|
|
||||||
worker.postMessage({
|
this.worker = this.startWorker(workerService);
|
||||||
request: 'index',
|
this.indexOnMutation(topic);
|
||||||
model: model,
|
|
||||||
id: domainObject.getId()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(model.composition)) {
|
ROOTS.forEach(function indexRoot(rootId) {
|
||||||
scheduleIdsForIndexing(model.composition);
|
provider.scheduleForIndexing(rootId);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (event.data.request === 'search') {
|
|
||||||
// Convert the ids given from the web worker into domain objects
|
|
||||||
for (id in event.data.results) {
|
|
||||||
ids.push(id);
|
|
||||||
}
|
|
||||||
objectService.getObjects(ids).then(function (objects) {
|
|
||||||
var searchResults = [],
|
|
||||||
id;
|
|
||||||
|
|
||||||
// Create searchResult objects
|
/**
|
||||||
for (id in objects) {
|
* Maximum number of concurrent index requests to allow.
|
||||||
searchResults.push({
|
*/
|
||||||
object: objects[id],
|
GenericSearchProvider.prototype.MAX_CONCURRENT_REQUESTS = 100;
|
||||||
id: id,
|
|
||||||
score: event.data.results[id]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resove the promise corresponding to this
|
/**
|
||||||
pendingQueries[event.data.timestamp].resolve({
|
* Query the search provider for results.
|
||||||
hits: searchResults,
|
*
|
||||||
total: event.data.total,
|
* @param {String} input the string to search by.
|
||||||
timedOut: event.data.timedOut
|
* @param {Number} maxResults max number of results to return.
|
||||||
});
|
* @returns {Promise} a promise for a modelResults object.
|
||||||
});
|
*/
|
||||||
}
|
GenericSearchProvider.prototype.query = function (
|
||||||
}
|
input,
|
||||||
|
maxResults
|
||||||
|
) {
|
||||||
|
|
||||||
function requestAndIndex(id) {
|
var queryId = this.dispatchSearch(input, maxResults),
|
||||||
pendingRequests += 1;
|
pendingQuery = this.$q.defer();
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleFlush = throttle(function flush() {
|
this.pendingQueries[queryId] = pendingQuery;
|
||||||
var batchSize =
|
|
||||||
Math.max(MAX_CONCURRENT_REQUESTS - pendingRequests, 0);
|
|
||||||
|
|
||||||
if (toRequest.length + pendingRequests < 1) {
|
return pendingQuery.promise;
|
||||||
$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;
|
/**
|
||||||
|
* Creates a search worker and attaches handlers.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param workerService
|
||||||
|
* @returns worker the created search worker.
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.startWorker = function (workerService) {
|
||||||
|
var worker = workerService.run('genericSearchWorker'),
|
||||||
|
provider = this;
|
||||||
|
|
||||||
// Index the tree's contents once at the beginning
|
worker.addEventListener('message', function (messageEvent) {
|
||||||
scheduleIdsForIndexing(ROOTS);
|
provider.onWorkerMessage(messageEvent);
|
||||||
|
});
|
||||||
|
|
||||||
// Re-index items when they are mutated
|
return worker;
|
||||||
mutationTopic.listen(function (domainObject) {
|
};
|
||||||
var id = domainObject.getId();
|
|
||||||
indexed[id] = false;
|
/**
|
||||||
scheduleIdsForIndexing([id]);
|
* Listen to the mutation topic and re-index objects when they are
|
||||||
|
* mutated.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param topic the topicService.
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
|
||||||
|
var mutationTopic = topic('mutation'),
|
||||||
|
provider = this;
|
||||||
|
|
||||||
|
mutationTopic.listen(function (mutatedObject) {
|
||||||
|
var id = mutatedObject.getId();
|
||||||
|
provider.indexedIds[id] = false;
|
||||||
|
provider.scheduleForIndexing(id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule an id to be indexed at a later date. If there are less
|
||||||
|
* pending requests then allowed, will kick off an indexing request.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {String} id to be indexed.
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
||||||
|
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||||
|
this.indexedIds[id] = true;
|
||||||
|
this.pendingIndex[id] = true;
|
||||||
|
this.idsToIndex.push(id);
|
||||||
|
}
|
||||||
|
this.keepIndexing();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are less pending requests than concurrent requests, keep
|
||||||
|
* firing requests.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.keepIndexing = function () {
|
||||||
|
while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS &&
|
||||||
|
this.idsToIndex.length
|
||||||
|
) {
|
||||||
|
this.beginIndexRequest();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass an id and model to the worker to be indexed. If the model has
|
||||||
|
* composition, schedule those ids for later indexing.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param id a model id
|
||||||
|
* @param model a model
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.index = function (id, model) {
|
||||||
|
var provider = this;
|
||||||
|
|
||||||
|
this.worker.postMessage({
|
||||||
|
request: 'index',
|
||||||
|
model: model,
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(model.composition)) {
|
||||||
|
model.composition.forEach(function (id) {
|
||||||
|
provider.scheduleForIndexing(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches through the filetree for domain objects which match
|
* Pulls an id from the indexing queue, loads it from the model service,
|
||||||
* the search term. This function is to be used as a fallback
|
* and indexes it. Upon completion, tells the provider to keep
|
||||||
* in the case where other search services are not avaliable.
|
* indexing.
|
||||||
* Returns a promise for a result object that has the format
|
*
|
||||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
* @private
|
||||||
* where a searchResult has the format
|
*/
|
||||||
* {id: string, object: domainObject, score: number}
|
GenericSearchProvider.prototype.beginIndexRequest = function () {
|
||||||
*
|
var idToIndex = this.idsToIndex.shift(),
|
||||||
* Notes:
|
provider = this;
|
||||||
* * The order of the results is not guarenteed.
|
|
||||||
* * A domain object qualifies as a match for a search input if
|
|
||||||
* the object's name property contains any of the search terms
|
|
||||||
* (which are generated by splitting the input at spaces).
|
|
||||||
* * Scores are higher for matches that have more of the terms
|
|
||||||
* as substrings.
|
|
||||||
*
|
|
||||||
* @param input The text input that is the query.
|
|
||||||
* @param timestamp The time at which this function was called.
|
|
||||||
* This timestamp is used as a unique identifier for this
|
|
||||||
* query and the corresponding results.
|
|
||||||
* @param maxResults (optional) The maximum number of results
|
|
||||||
* that this function should return.
|
|
||||||
* @param timeout (optional) The time after which the search should
|
|
||||||
* stop calculations and return partial results.
|
|
||||||
*/
|
|
||||||
GenericSearchProvider.prototype.query = function query(input, timestamp, maxResults, timeout) {
|
|
||||||
var terms = [],
|
|
||||||
searchResults = [],
|
|
||||||
pendingQueries = this.pendingQueries,
|
|
||||||
worker = this.worker,
|
|
||||||
defer = this.$q.defer();
|
|
||||||
|
|
||||||
// Tell the worker to search for items it has that match this searchInput.
|
this.pendingRequests += 1;
|
||||||
// Takes the searchInput, as well as a max number of results (will return
|
this.modelService
|
||||||
// less than that if there are fewer matches).
|
.getModels([idToIndex])
|
||||||
function workerSearch(searchInput, maxResults, timestamp, timeout) {
|
.then(function (models) {
|
||||||
var message = {
|
delete provider.pendingIndex[idToIndex];
|
||||||
request: 'search',
|
if (models[idToIndex]) {
|
||||||
input: searchInput,
|
provider.index(idToIndex, models[idToIndex]);
|
||||||
maxNumber: maxResults,
|
|
||||||
timestamp: timestamp,
|
|
||||||
timeout: timeout
|
|
||||||
};
|
|
||||||
worker.postMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the input is nonempty, do a search
|
|
||||||
if (input !== '' && input !== undefined) {
|
|
||||||
|
|
||||||
// Allow us to access this promise later to resolve it later
|
|
||||||
pendingQueries[timestamp] = defer;
|
|
||||||
|
|
||||||
// Check to see if the user provided a maximum
|
|
||||||
// number of results to display
|
|
||||||
if (!maxResults) {
|
|
||||||
// Else, we provide a default value
|
|
||||||
maxResults = DEFAULT_MAX_RESULTS;
|
|
||||||
}
|
|
||||||
// Similarly, check if timeout was provided
|
|
||||||
if (!timeout) {
|
|
||||||
timeout = DEFAULT_TIMEOUT;
|
|
||||||
}
|
}
|
||||||
|
}, function () {
|
||||||
|
provider
|
||||||
|
.$log
|
||||||
|
.warn('Failed to index domain object ' + idToIndex);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
setTimeout(function () {
|
||||||
|
provider.pendingRequests -= 1;
|
||||||
|
provider.keepIndexing();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Send the query to the worker
|
/**
|
||||||
workerSearch(input, maxResults, timestamp, timeout);
|
* Handle messages from the worker. Only really knows how to handle search
|
||||||
|
* results, which are parsed, transformed into a modelResult object, which
|
||||||
|
* is used to resolve the corresponding promise.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.onWorkerMessage = function (event) {
|
||||||
|
if (event.data.request !== 'search') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return defer.promise;
|
var pendingQuery = this.pendingQueries[event.data.queryId],
|
||||||
} else {
|
modelResults = {
|
||||||
// Otherwise return an empty result
|
total: event.data.total
|
||||||
return { hits: [], total: 0 };
|
};
|
||||||
}
|
|
||||||
};
|
modelResults.hits = event.data.results.map(function (hit) {
|
||||||
|
return {
|
||||||
|
id: hit.item.id,
|
||||||
|
model: hit.item.model,
|
||||||
|
score: hit.matchCount
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingQuery.resolve(modelResults);
|
||||||
|
delete this.pendingQueries[event.data.queryId];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @returns {Number} a unique, unusued query Id.
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.makeQueryId = function () {
|
||||||
|
var queryId = Math.ceil(Math.random() * 100000);
|
||||||
|
while (this.pendingQueries[queryId]) {
|
||||||
|
queryId = Math.ceil(Math.random() * 100000);
|
||||||
|
}
|
||||||
|
return queryId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a search query to the worker and return a queryId.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Number} a unique query Id for the query.
|
||||||
|
*/
|
||||||
|
GenericSearchProvider.prototype.dispatchSearch = function (
|
||||||
|
searchInput,
|
||||||
|
maxResults
|
||||||
|
) {
|
||||||
|
var queryId = this.makeQueryId();
|
||||||
|
|
||||||
|
this.worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: searchInput,
|
||||||
|
maxResults: maxResults,
|
||||||
|
queryId: queryId
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryId;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return GenericSearchProvider;
|
return GenericSearchProvider;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
@ -29,128 +29,127 @@
|
|||||||
|
|
||||||
// An array of objects composed of domain object IDs and models
|
// An array of objects composed of domain object IDs and models
|
||||||
// {id: domainObject's ID, model: domainObject's model}
|
// {id: domainObject's ID, model: domainObject's model}
|
||||||
var indexedItems = [];
|
var indexedItems = [],
|
||||||
|
TERM_SPLITTER = /[ _\*]/;
|
||||||
|
|
||||||
// Helper function for serach()
|
function indexItem(id, model) {
|
||||||
function convertToTerms(input) {
|
var vector = {
|
||||||
var terms = input;
|
name: model.name
|
||||||
// Shave any spaces off of the ends of the input
|
};
|
||||||
while (terms.substr(0, 1) === ' ') {
|
vector.cleanName = model.name.trim();
|
||||||
terms = terms.substring(1, terms.length);
|
vector.lowerCaseName = vector.cleanName.toLocaleLowerCase();
|
||||||
}
|
vector.terms = vector.lowerCaseName.split(TERM_SPLITTER);
|
||||||
while (terms.substr(terms.length - 1, 1) === ' ') {
|
|
||||||
terms = terms.substring(0, terms.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then split it at spaces and asterisks
|
indexedItems.push({
|
||||||
terms = terms.split(/ |\*/);
|
id: id,
|
||||||
|
vector: vector,
|
||||||
// Remove any empty strings from the terms
|
model: model
|
||||||
while (terms.indexOf('') !== -1) {
|
});
|
||||||
terms.splice(terms.indexOf(''), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return terms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for search()
|
// Helper function for search()
|
||||||
function scoreItem(item, input, terms) {
|
function convertToTerms(input) {
|
||||||
var name = item.model.name.toLocaleLowerCase(),
|
var query = {
|
||||||
weight = 0.65,
|
exactInput: input
|
||||||
score = 0.0,
|
};
|
||||||
i;
|
query.inputClean = input.trim();
|
||||||
|
query.inputLowerCase = query.inputClean.toLocaleLowerCase();
|
||||||
// Make the score really big if the item name and
|
query.terms = query.inputLowerCase.split(TERM_SPLITTER);
|
||||||
// the original search input are the same
|
query.exactTerms = query.inputClean.split(TERM_SPLITTER);
|
||||||
if (name === input) {
|
return query;
|
||||||
score = 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < terms.length; i += 1) {
|
|
||||||
// Increase the score if the term is in the item name
|
|
||||||
if (name.indexOf(terms[i]) !== -1) {
|
|
||||||
score += 1;
|
|
||||||
|
|
||||||
// Add extra to the score if the search term exists
|
|
||||||
// as its own term within the items
|
|
||||||
if (name.split(' ').indexOf(terms[i]) !== -1) {
|
|
||||||
score += 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return score * weight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets search results from the indexedItems based on provided search
|
* Gets search results from the indexedItems based on provided search
|
||||||
* input. Returns matching results from indexedItems, as well as the
|
* input. Returns matching results from indexedItems
|
||||||
* timestamp that was passed to it.
|
|
||||||
*
|
*
|
||||||
* @param data An object which contains:
|
* @param data An object which contains:
|
||||||
* * input: The original string which we are searching with
|
* * input: The original string which we are searching with
|
||||||
* * maxNumber: The maximum number of search results desired
|
* * maxResults: The maximum number of search results desired
|
||||||
* * timestamp: The time identifier from when the query was made
|
* * queryId: an id identifying this query, will be returned.
|
||||||
*/
|
*/
|
||||||
function search(data) {
|
function search(data) {
|
||||||
// This results dictionary will have domain object ID keys which
|
// This results dictionary will have domain object ID keys which
|
||||||
// point to the value the domain object's score.
|
// point to the value the domain object's score.
|
||||||
var results = {},
|
var results,
|
||||||
input = data.input.toLocaleLowerCase(),
|
input = data.input,
|
||||||
terms = convertToTerms(input),
|
query = convertToTerms(input),
|
||||||
message = {
|
message = {
|
||||||
request: 'search',
|
request: 'search',
|
||||||
results: {},
|
results: {},
|
||||||
total: 0,
|
total: 0,
|
||||||
timestamp: data.timestamp,
|
queryId: data.queryId
|
||||||
timedOut: false
|
|
||||||
},
|
},
|
||||||
score,
|
matches = {};
|
||||||
i,
|
|
||||||
id;
|
|
||||||
|
|
||||||
// If the user input is empty, we want to have no search results.
|
if (!query.inputClean) {
|
||||||
if (input !== '') {
|
// No search terms, no results;
|
||||||
for (i = 0; i < indexedItems.length; i += 1) {
|
return message;
|
||||||
// If this is taking too long, then stop
|
|
||||||
if (Date.now() > data.timestamp + data.timeout) {
|
|
||||||
message.timedOut = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Score and add items
|
|
||||||
score = scoreItem(indexedItems[i], input, terms);
|
|
||||||
if (score > 0) {
|
|
||||||
results[indexedItems[i].id] = score;
|
|
||||||
message.total += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate results if there are more than maxResults
|
// Two phases: find matches, then score matches.
|
||||||
if (message.total > data.maxResults) {
|
// Idea being that match finding should be fast, so that future scoring
|
||||||
i = 0;
|
// operations process fewer objects.
|
||||||
for (id in results) {
|
|
||||||
message.results[id] = results[id];
|
query.terms.forEach(function findMatchingItems(term) {
|
||||||
i += 1;
|
indexedItems
|
||||||
if (i >= data.maxResults) {
|
.filter(function matchesItem(item) {
|
||||||
break;
|
return item.vector.lowerCaseName.indexOf(term) !== -1;
|
||||||
|
})
|
||||||
|
.forEach(function trackMatch(matchedItem) {
|
||||||
|
if (!matches[matchedItem.id]) {
|
||||||
|
matches[matchedItem.id] = {
|
||||||
|
matchCount: 0,
|
||||||
|
item: matchedItem
|
||||||
|
};
|
||||||
|
}
|
||||||
|
matches[matchedItem.id].matchCount += 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then, score matching items.
|
||||||
|
results = Object
|
||||||
|
.keys(matches)
|
||||||
|
.map(function asMatches(matchId) {
|
||||||
|
return matches[matchId];
|
||||||
|
})
|
||||||
|
.map(function prioritizeExactMatches(match) {
|
||||||
|
if (match.item.vector.name === query.exactInput) {
|
||||||
|
match.matchCount += 100;
|
||||||
|
} else if (match.item.vector.lowerCaseName ===
|
||||||
|
query.inputLowerCase) {
|
||||||
|
match.matchCount += 50;
|
||||||
}
|
}
|
||||||
}
|
return match;
|
||||||
// TODO: This seems inefficient.
|
})
|
||||||
} else {
|
.map(function prioritizeCompleteTermMatches(match) {
|
||||||
message.results = results;
|
match.item.vector.terms.forEach(function (term) {
|
||||||
}
|
if (query.terms.indexOf(term) !== -1) {
|
||||||
|
match.matchCount += 0.5;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return match;
|
||||||
|
})
|
||||||
|
.sort(function compare(a, b) {
|
||||||
|
if (a.matchCount > b.matchCount) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.matchCount < b.matchCount) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, data.maxResults);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = function (event) {
|
self.onmessage = function (event) {
|
||||||
if (event.data.request === 'index') {
|
if (event.data.request === 'index') {
|
||||||
indexedItems.push({
|
indexItem(event.data.id, event.data.model);
|
||||||
id: event.data.id,
|
|
||||||
model: event.data.model
|
|
||||||
});
|
|
||||||
} else if (event.data.request === 'search') {
|
} else if (event.data.request === 'search') {
|
||||||
self.postMessage(search(event.data));
|
self.postMessage(search(event.data));
|
||||||
}
|
}
|
||||||
|
@ -24,122 +24,201 @@
|
|||||||
/**
|
/**
|
||||||
* Module defining SearchAggregator. Created by shale on 07/16/2015.
|
* Module defining SearchAggregator. Created by shale on 07/16/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var DEFUALT_TIMEOUT = 1000,
|
], function (
|
||||||
DEFAULT_MAX_RESULTS = 100;
|
|
||||||
|
|
||||||
/**
|
) {
|
||||||
* Allows multiple services which provide search functionality
|
"use strict";
|
||||||
* to be treated as one.
|
|
||||||
*
|
/**
|
||||||
* @constructor
|
* Aggregates multiple search providers as a singular search provider.
|
||||||
* @param $q Angular's $q, for promise consolidation.
|
* Search providers are expected to implement a `query` method which returns
|
||||||
* @param {SearchProvider[]} providers The search providers to be
|
* a promise for a `modelResults` object.
|
||||||
* aggregated.
|
*
|
||||||
*/
|
* The search aggregator combines the results from multiple providers,
|
||||||
function SearchAggregator($q, providers) {
|
* removes aggregates, and converts the results to domain objects.
|
||||||
this.$q = $q;
|
*
|
||||||
this.providers = providers;
|
* @constructor
|
||||||
|
* @param $q Angular's $q, for promise consolidation.
|
||||||
|
* @param objectService
|
||||||
|
* @param {SearchProvider[]} providers The search providers to be
|
||||||
|
* aggregated.
|
||||||
|
*/
|
||||||
|
function SearchAggregator($q, objectService, providers) {
|
||||||
|
this.$q = $q;
|
||||||
|
this.objectService = objectService;
|
||||||
|
this.providers = providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If max results is not specified in query, use this as default.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.DEFAULT_MAX_RESULTS = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because filtering isn't implemented inside each provider, the fudge
|
||||||
|
* factor is a multiplier on the number of results returned-- more results
|
||||||
|
* than requested will be fetched, and then will be filtered. This helps
|
||||||
|
* provide more predictable pagination when large numbers of results are
|
||||||
|
* returned but very few results match filters.
|
||||||
|
*
|
||||||
|
* If a provider level filter implementation is implemented in the future,
|
||||||
|
* remove this.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.FUDGE_FACTOR = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a query to each of the providers. Returns a promise for
|
||||||
|
* a result object that has the format
|
||||||
|
* {hits: searchResult[], total: number}
|
||||||
|
* where a searchResult has the format
|
||||||
|
* {id: string, object: domainObject, score: number}
|
||||||
|
*
|
||||||
|
* @param {String} inputText The text input that is the query.
|
||||||
|
* @param {Number} maxResults (optional) The maximum number of results
|
||||||
|
* that this function should return. If not provided, a
|
||||||
|
* default of 100 will be used.
|
||||||
|
* @param {Function} [filter] if provided, will be called for every
|
||||||
|
* potential modelResult. If it returns false, the model result will be
|
||||||
|
* excluded from the search results.
|
||||||
|
* @returns {Promise} A Promise for a search result object.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.query = function (
|
||||||
|
inputText,
|
||||||
|
maxResults,
|
||||||
|
filter
|
||||||
|
) {
|
||||||
|
|
||||||
|
var aggregator = this,
|
||||||
|
resultPromises;
|
||||||
|
|
||||||
|
if (!maxResults) {
|
||||||
|
maxResults = this.DEFAULT_MAX_RESULTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
resultPromises = this.providers.map(function (provider) {
|
||||||
* Sends a query to each of the providers. Returns a promise for
|
return provider.query(
|
||||||
* a result object that has the format
|
inputText,
|
||||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
maxResults * aggregator.FUDGE_FACTOR
|
||||||
* where a searchResult has the format
|
);
|
||||||
* {id: string, object: domainObject, score: number}
|
});
|
||||||
*
|
|
||||||
* @param inputText The text input that is the query.
|
|
||||||
* @param maxResults (optional) The maximum number of results
|
|
||||||
* that this function should return. If not provided, a
|
|
||||||
* default of 100 will be used.
|
|
||||||
*/
|
|
||||||
SearchAggregator.prototype.query = function queryAll(inputText, maxResults) {
|
|
||||||
var $q = this.$q,
|
|
||||||
providers = this.providers,
|
|
||||||
i,
|
|
||||||
timestamp = Date.now(),
|
|
||||||
resultPromises = [];
|
|
||||||
|
|
||||||
// Remove duplicate objects that have the same ID. Modifies the passed
|
return this.$q
|
||||||
// array, and returns the number that were removed.
|
.all(resultPromises)
|
||||||
function filterDuplicates(results, total) {
|
.then(function (providerResults) {
|
||||||
var ids = {},
|
var modelResults = {
|
||||||
numRemoved = 0,
|
hits: [],
|
||||||
i;
|
total: 0
|
||||||
|
};
|
||||||
|
|
||||||
for (i = 0; i < results.length; i += 1) {
|
providerResults.forEach(function (providerResult) {
|
||||||
if (ids[results[i].id]) {
|
modelResults.hits =
|
||||||
// If this result's ID is already there, remove the object
|
modelResults.hits.concat(providerResult.hits);
|
||||||
results.splice(i, 1);
|
modelResults.total += providerResult.total;
|
||||||
numRemoved += 1;
|
|
||||||
|
|
||||||
// Reduce loop index because we shortened the array
|
|
||||||
i -= 1;
|
|
||||||
} else {
|
|
||||||
// Otherwise add the ID to the list of the ones we have seen
|
|
||||||
ids[results[i].id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return numRemoved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order the objects from highest to lowest score in the array.
|
|
||||||
// Modifies the passed array, as well as returns the modified array.
|
|
||||||
function orderByScore(results) {
|
|
||||||
results.sort(function (a, b) {
|
|
||||||
if (a.score > b.score) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.score > a.score) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!maxResults) {
|
modelResults = aggregator.orderByScore(modelResults);
|
||||||
maxResults = DEFAULT_MAX_RESULTS;
|
modelResults = aggregator.applyFilter(modelResults, filter);
|
||||||
}
|
modelResults = aggregator.removeDuplicates(modelResults);
|
||||||
|
|
||||||
// Send the query to all the providers
|
return aggregator.asObjectResults(modelResults);
|
||||||
for (i = 0; i < providers.length; i += 1) {
|
|
||||||
resultPromises.push(
|
|
||||||
providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get promises for results arrays
|
|
||||||
return $q.all(resultPromises).then(function (resultObjects) {
|
|
||||||
var results = [],
|
|
||||||
totalSum = 0,
|
|
||||||
i;
|
|
||||||
|
|
||||||
// Merge results
|
|
||||||
for (i = 0; i < resultObjects.length; i += 1) {
|
|
||||||
results = results.concat(resultObjects[i].hits);
|
|
||||||
totalSum += resultObjects[i].total;
|
|
||||||
}
|
|
||||||
// Order by score first, so that when removing repeats we keep the higher scored ones
|
|
||||||
orderByScore(results);
|
|
||||||
totalSum -= filterDuplicates(results, totalSum);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hits: results,
|
|
||||||
total: totalSum,
|
|
||||||
timedOut: resultObjects.some(function (obj) {
|
|
||||||
return obj.timedOut;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return SearchAggregator;
|
/**
|
||||||
}
|
* Order model results by score descending and return them.
|
||||||
);
|
*/
|
||||||
|
SearchAggregator.prototype.orderByScore = function (modelResults) {
|
||||||
|
modelResults.hits.sort(function (a, b) {
|
||||||
|
if (a.score > b.score) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.score > a.score) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return modelResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a filter to each model result, removing it from search results
|
||||||
|
* if it does not match.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.applyFilter = function (modelResults, filter) {
|
||||||
|
if (!filter) {
|
||||||
|
return modelResults;
|
||||||
|
}
|
||||||
|
var initialLength = modelResults.hits.length,
|
||||||
|
finalLength,
|
||||||
|
removedByFilter;
|
||||||
|
|
||||||
|
modelResults.hits = modelResults.hits.filter(function (hit) {
|
||||||
|
return filter(hit.model);
|
||||||
|
});
|
||||||
|
|
||||||
|
finalLength = modelResults.hits.length;
|
||||||
|
removedByFilter = initialLength - finalLength;
|
||||||
|
modelResults.total -= removedByFilter;
|
||||||
|
|
||||||
|
return modelResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove duplicate hits in a modelResults object, and decrement `total`
|
||||||
|
* each time a duplicate is removed.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.removeDuplicates = function (modelResults) {
|
||||||
|
var includedIds = {};
|
||||||
|
|
||||||
|
modelResults.hits = modelResults
|
||||||
|
.hits
|
||||||
|
.filter(function alreadyInResults(hit) {
|
||||||
|
if (includedIds[hit.id]) {
|
||||||
|
modelResults.total -= 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
includedIds[hit.id] = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return modelResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert modelResults to objectResults by fetching them from the object
|
||||||
|
* service.
|
||||||
|
*
|
||||||
|
* @returns {Promise} for an objectResults object.
|
||||||
|
*/
|
||||||
|
SearchAggregator.prototype.asObjectResults = function (modelResults) {
|
||||||
|
var objectIds = modelResults.hits.map(function (modelResult) {
|
||||||
|
return modelResult.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this
|
||||||
|
.objectService
|
||||||
|
.getObjects(objectIds)
|
||||||
|
.then(function (objects) {
|
||||||
|
|
||||||
|
var objectResults = {
|
||||||
|
total: modelResults.total
|
||||||
|
};
|
||||||
|
|
||||||
|
objectResults.hits = modelResults
|
||||||
|
.hits
|
||||||
|
.map(function asObjectResult(hit) {
|
||||||
|
return {
|
||||||
|
id: hit.id,
|
||||||
|
object: objects[hit.id],
|
||||||
|
score: hit.score
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return objectResults;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return SearchAggregator;
|
||||||
|
});
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* Administration. All rights reserved.
|
* Administration. All rights reserved.
|
||||||
*
|
*
|
||||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
* 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.
|
* 'License'); you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
@ -24,185 +24,162 @@
|
|||||||
/**
|
/**
|
||||||
* SearchSpec. Created by shale on 07/31/2015.
|
* SearchSpec. Created by shale on 07/31/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
["../../src/controllers/SearchController"],
|
'../../src/controllers/SearchController'
|
||||||
function (SearchController) {
|
], function (
|
||||||
"use strict";
|
SearchController
|
||||||
|
) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// These should be the same as the ones on the top of the search controller
|
describe('The search controller', function () {
|
||||||
var INITIAL_LOAD_NUMBER = 20,
|
var mockScope,
|
||||||
LOAD_INCREMENT = 20;
|
mockSearchService,
|
||||||
|
mockPromise,
|
||||||
|
mockSearchResult,
|
||||||
|
mockDomainObject,
|
||||||
|
mockTypes,
|
||||||
|
controller;
|
||||||
|
|
||||||
describe("The search controller", function () {
|
function bigArray(size) {
|
||||||
var mockScope,
|
var array = [],
|
||||||
mockSearchService,
|
i;
|
||||||
mockPromise,
|
for (i = 0; i < size; i += 1) {
|
||||||
mockSearchResult,
|
array.push(mockSearchResult);
|
||||||
mockDomainObject,
|
|
||||||
mockTypes,
|
|
||||||
controller;
|
|
||||||
|
|
||||||
function bigArray(size) {
|
|
||||||
var array = [],
|
|
||||||
i;
|
|
||||||
for (i = 0; i < size; i += 1) {
|
|
||||||
array.push(mockSearchResult);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockScope = jasmine.createSpyObj(
|
mockScope = jasmine.createSpyObj(
|
||||||
"$scope",
|
'$scope',
|
||||||
[ "$watch" ]
|
[ '$watch' ]
|
||||||
);
|
);
|
||||||
mockScope.ngModel = {};
|
mockScope.ngModel = {};
|
||||||
mockScope.ngModel.input = "test input";
|
mockScope.ngModel.input = 'test input';
|
||||||
mockScope.ngModel.checked = {};
|
mockScope.ngModel.checked = {};
|
||||||
mockScope.ngModel.checked['mock.type'] = true;
|
mockScope.ngModel.checked['mock.type'] = true;
|
||||||
|
mockScope.ngModel.checkAll = true;
|
||||||
|
|
||||||
|
mockSearchService = jasmine.createSpyObj(
|
||||||
|
'searchService',
|
||||||
|
[ 'query' ]
|
||||||
|
);
|
||||||
|
mockPromise = jasmine.createSpyObj(
|
||||||
|
'promise',
|
||||||
|
[ 'then' ]
|
||||||
|
);
|
||||||
|
mockSearchService.query.andReturn(mockPromise);
|
||||||
|
|
||||||
|
mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
|
||||||
|
|
||||||
|
mockSearchResult = jasmine.createSpyObj(
|
||||||
|
'searchResult',
|
||||||
|
[ '' ]
|
||||||
|
);
|
||||||
|
mockDomainObject = jasmine.createSpyObj(
|
||||||
|
'domainObject',
|
||||||
|
[ 'getModel' ]
|
||||||
|
);
|
||||||
|
mockSearchResult.object = mockDomainObject;
|
||||||
|
mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
|
||||||
|
|
||||||
|
controller = new SearchController(mockScope, mockSearchService, mockTypes);
|
||||||
|
controller.search();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a default number of results per page', function () {
|
||||||
|
expect(controller.RESULTS_PER_PAGE).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends queries to the search service', function () {
|
||||||
|
expect(mockSearchService.query).toHaveBeenCalledWith(
|
||||||
|
'test input',
|
||||||
|
controller.RESULTS_PER_PAGE,
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter query function', function () {
|
||||||
|
it('returns true when all types allowed', function () {
|
||||||
mockScope.ngModel.checkAll = true;
|
mockScope.ngModel.checkAll = true;
|
||||||
|
controller.onFilterChange();
|
||||||
mockSearchService = jasmine.createSpyObj(
|
var filterFn = mockSearchService.query.mostRecentCall.args[2];
|
||||||
"searchService",
|
expect(filterFn('askbfa')).toBe(true);
|
||||||
[ "query" ]
|
|
||||||
);
|
|
||||||
mockPromise = jasmine.createSpyObj(
|
|
||||||
"promise",
|
|
||||||
[ "then" ]
|
|
||||||
);
|
|
||||||
mockSearchService.query.andReturn(mockPromise);
|
|
||||||
|
|
||||||
mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
|
|
||||||
|
|
||||||
mockSearchResult = jasmine.createSpyObj(
|
|
||||||
"searchResult",
|
|
||||||
[ "" ]
|
|
||||||
);
|
|
||||||
mockDomainObject = jasmine.createSpyObj(
|
|
||||||
"domainObject",
|
|
||||||
[ "getModel" ]
|
|
||||||
);
|
|
||||||
mockSearchResult.object = mockDomainObject;
|
|
||||||
mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
|
|
||||||
|
|
||||||
controller = new SearchController(mockScope, mockSearchService, mockTypes);
|
|
||||||
controller.search();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends queries to the search service", function () {
|
it('returns true only for matching checked types', function () {
|
||||||
expect(mockSearchService.query).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("populates the results with results from the search service", function () {
|
|
||||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
|
||||||
|
|
||||||
expect(mockScope.results).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is loading until the service's promise fufills", function () {
|
|
||||||
// Send query
|
|
||||||
controller.search();
|
|
||||||
expect(mockScope.loading).toBeTruthy();
|
|
||||||
|
|
||||||
// Then resolve the promises
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
|
||||||
expect(mockScope.loading).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("displays only some results when there are many", function () {
|
|
||||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)});
|
|
||||||
|
|
||||||
expect(mockScope.results).toBeDefined();
|
|
||||||
expect(mockScope.results.length).toBeLessThan(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects when there are more results", function () {
|
|
||||||
mockScope.ngModel.checkAll = false;
|
mockScope.ngModel.checkAll = false;
|
||||||
|
controller.onFilterChange();
|
||||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
var filterFn = mockSearchService.query.mostRecentCall.args[2];
|
||||||
mockPromise.then.mostRecentCall.args[0]({
|
expect(filterFn({type: 'mock.type'})).toBe(true);
|
||||||
hits: bigArray(INITIAL_LOAD_NUMBER + 5),
|
expect(filterFn({type: 'other.type'})).toBe(false);
|
||||||
total: INITIAL_LOAD_NUMBER + 5
|
|
||||||
});
|
|
||||||
// bigArray gives searchResults of type 'mock.type'
|
|
||||||
mockScope.ngModel.checked['mock.type'] = false;
|
|
||||||
mockScope.ngModel.checked['mock.type.2'] = true;
|
|
||||||
|
|
||||||
expect(controller.areMore()).toBeFalsy();
|
|
||||||
|
|
||||||
mockScope.ngModel.checked['mock.type'] = true;
|
|
||||||
|
|
||||||
expect(controller.areMore()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can load more results", function () {
|
|
||||||
var oldSize;
|
|
||||||
|
|
||||||
expect(mockPromise.then).toHaveBeenCalled();
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({
|
|
||||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
|
||||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
|
||||||
});
|
|
||||||
// These hits and total lengths are the case where the controller
|
|
||||||
// DOES NOT have to re-search to load more results
|
|
||||||
oldSize = mockScope.results.length;
|
|
||||||
|
|
||||||
expect(controller.areMore()).toBeTruthy();
|
|
||||||
|
|
||||||
controller.loadMore();
|
|
||||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can re-search to load more results", function () {
|
|
||||||
var oldSize,
|
|
||||||
oldCallCount;
|
|
||||||
|
|
||||||
expect(mockPromise.then).toHaveBeenCalled();
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({
|
|
||||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT - 1),
|
|
||||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
|
||||||
});
|
|
||||||
// These hits and total lengths are the case where the controller
|
|
||||||
// DOES have to re-search to load more results
|
|
||||||
oldSize = mockScope.results.length;
|
|
||||||
oldCallCount = mockPromise.then.callCount;
|
|
||||||
expect(controller.areMore()).toBeTruthy();
|
|
||||||
|
|
||||||
controller.loadMore();
|
|
||||||
expect(mockPromise.then).toHaveBeenCalled();
|
|
||||||
// Make sure that a NEW call to search has been made
|
|
||||||
expect(oldCallCount).toBeLessThan(mockPromise.then.callCount);
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({
|
|
||||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
|
||||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
|
||||||
});
|
|
||||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the ngModel.search flag", function () {
|
|
||||||
// Flag should be true with nonempty input
|
|
||||||
expect(mockScope.ngModel.search).toEqual(true);
|
|
||||||
|
|
||||||
// Flag should be flase with empty input
|
|
||||||
mockScope.ngModel.input = "";
|
|
||||||
controller.search();
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
|
||||||
expect(mockScope.ngModel.search).toEqual(false);
|
|
||||||
|
|
||||||
// Both the empty string and undefined should be 'empty input'
|
|
||||||
mockScope.ngModel.input = undefined;
|
|
||||||
controller.search();
|
|
||||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
|
||||||
expect(mockScope.ngModel.search).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has a default results list to filter from", function () {
|
|
||||||
expect(mockScope.ngModel.filter()).toBeDefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
it('populates the results with results from the search service', function () {
|
||||||
|
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({hits: ['a']});
|
||||||
|
|
||||||
|
expect(mockScope.results.length).toBe(1);
|
||||||
|
expect(mockScope.results).toContain('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is loading until the service\'s promise fufills', function () {
|
||||||
|
expect(mockScope.loading).toBeTruthy();
|
||||||
|
|
||||||
|
// Then resolve the promises
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
||||||
|
expect(mockScope.loading).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects when there are more results', function () {
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({
|
||||||
|
hits: bigArray(controller.RESULTS_PER_PAGE),
|
||||||
|
total: controller.RESULTS_PER_PAGE + 5
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockScope.results.length).toBe(controller.RESULTS_PER_PAGE);
|
||||||
|
expect(controller.areMore()).toBeTruthy();
|
||||||
|
|
||||||
|
controller.loadMore();
|
||||||
|
|
||||||
|
expect(mockSearchService.query).toHaveBeenCalledWith(
|
||||||
|
'test input',
|
||||||
|
controller.RESULTS_PER_PAGE * 2,
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({
|
||||||
|
hits: bigArray(controller.RESULTS_PER_PAGE + 5),
|
||||||
|
total: controller.RESULTS_PER_PAGE + 5
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockScope.results.length)
|
||||||
|
.toBe(controller.RESULTS_PER_PAGE + 5);
|
||||||
|
|
||||||
|
expect(controller.areMore()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the ngModel.search flag', function () {
|
||||||
|
// Flag should be true with nonempty input
|
||||||
|
expect(mockScope.ngModel.search).toEqual(true);
|
||||||
|
|
||||||
|
// Flag should be flase with empty input
|
||||||
|
mockScope.ngModel.input = '';
|
||||||
|
controller.search();
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||||
|
expect(mockScope.ngModel.search).toEqual(false);
|
||||||
|
|
||||||
|
// Both the empty string and undefined should be 'empty input'
|
||||||
|
mockScope.ngModel.input = undefined;
|
||||||
|
controller.search();
|
||||||
|
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||||
|
expect(mockScope.ngModel.search).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attaches a filter function to scope', function () {
|
||||||
|
expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -19,275 +19,321 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
/*global define,describe,it,expect,beforeEach,jasmine,Promise,spyOn,waitsFor,
|
||||||
|
runs*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchSpec. Created by shale on 07/31/2015.
|
* SearchSpec. Created by shale on 07/31/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
["../../src/services/GenericSearchProvider"],
|
"../../src/services/GenericSearchProvider"
|
||||||
function (GenericSearchProvider) {
|
], function (
|
||||||
"use strict";
|
GenericSearchProvider
|
||||||
|
) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
describe("The generic search provider ", function () {
|
describe('GenericSearchProvider', function () {
|
||||||
var mockQ,
|
var $q,
|
||||||
mockLog,
|
$log,
|
||||||
mockThrottle,
|
modelService,
|
||||||
mockDeferred,
|
models,
|
||||||
mockObjectService,
|
workerService,
|
||||||
mockObjectPromise,
|
worker,
|
||||||
mockChainedPromise,
|
topic,
|
||||||
mockDomainObjects,
|
mutationTopic,
|
||||||
mockCapability,
|
ROOTS,
|
||||||
mockCapabilityPromise,
|
provider;
|
||||||
mockWorkerService,
|
|
||||||
mockWorker,
|
|
||||||
mockTopic,
|
|
||||||
mockMutationTopic,
|
|
||||||
mockRoots = ['root1', 'root2'],
|
|
||||||
mockThrottledFn,
|
|
||||||
throttledCallCount,
|
|
||||||
provider,
|
|
||||||
mockProviderResults;
|
|
||||||
|
|
||||||
function resolveObjectPromises() {
|
beforeEach(function () {
|
||||||
var i;
|
$q = jasmine.createSpyObj(
|
||||||
for (i = 0; i < mockObjectPromise.then.calls.length; i += 1) {
|
'$q',
|
||||||
mockChainedPromise.then.calls[i].args[0](
|
['defer']
|
||||||
mockObjectPromise.then.calls[i]
|
);
|
||||||
.args[0](mockDomainObjects)
|
$log = jasmine.createSpyObj(
|
||||||
);
|
'$log',
|
||||||
}
|
['warn']
|
||||||
}
|
);
|
||||||
|
models = {};
|
||||||
|
modelService = jasmine.createSpyObj(
|
||||||
|
'modelService',
|
||||||
|
['getModels']
|
||||||
|
);
|
||||||
|
modelService.getModels.andReturn(Promise.resolve(models));
|
||||||
|
workerService = jasmine.createSpyObj(
|
||||||
|
'workerService',
|
||||||
|
['run']
|
||||||
|
);
|
||||||
|
worker = jasmine.createSpyObj(
|
||||||
|
'worker',
|
||||||
|
[
|
||||||
|
'postMessage',
|
||||||
|
'addEventListener'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
workerService.run.andReturn(worker);
|
||||||
|
topic = jasmine.createSpy('topic');
|
||||||
|
mutationTopic = jasmine.createSpyObj(
|
||||||
|
'mutationTopic',
|
||||||
|
['listen']
|
||||||
|
);
|
||||||
|
topic.andReturn(mutationTopic);
|
||||||
|
ROOTS = [
|
||||||
|
'mine'
|
||||||
|
];
|
||||||
|
|
||||||
function resolveThrottledFn() {
|
spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');
|
||||||
if (mockThrottledFn.calls.length > throttledCallCount) {
|
|
||||||
mockThrottle.mostRecentCall.args[0]();
|
|
||||||
throttledCallCount = mockThrottledFn.calls.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAsyncTasks() {
|
provider = new GenericSearchProvider(
|
||||||
resolveThrottledFn();
|
$q,
|
||||||
resolveObjectPromises();
|
$log,
|
||||||
}
|
modelService,
|
||||||
|
workerService,
|
||||||
|
topic,
|
||||||
|
ROOTS
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listens for general mutation', function () {
|
||||||
|
expect(topic).toHaveBeenCalledWith('mutation');
|
||||||
|
expect(mutationTopic.listen)
|
||||||
|
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reschedules indexing when mutation occurs', function () {
|
||||||
|
var mockDomainObject =
|
||||||
|
jasmine.createSpyObj('domainObj', ['getId']);
|
||||||
|
mockDomainObject.getId.andReturn("some-id");
|
||||||
|
mutationTopic.listen.mostRecentCall.args[0](mockDomainObject);
|
||||||
|
expect(provider.scheduleForIndexing).toHaveBeenCalledWith('some-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts indexing roots', function () {
|
||||||
|
expect(provider.scheduleForIndexing).toHaveBeenCalledWith('mine');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs a worker', function () {
|
||||||
|
expect(workerService.run)
|
||||||
|
.toHaveBeenCalledWith('genericSearchWorker');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listens for messages from worker', function () {
|
||||||
|
expect(worker.addEventListener)
|
||||||
|
.toHaveBeenCalledWith('message', jasmine.any(Function));
|
||||||
|
spyOn(provider, 'onWorkerMessage');
|
||||||
|
worker.addEventListener.mostRecentCall.args[1]('mymessage');
|
||||||
|
expect(provider.onWorkerMessage).toHaveBeenCalledWith('mymessage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a maximum number of concurrent requests', function () {
|
||||||
|
expect(provider.MAX_CONCURRENT_REQUESTS).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scheduleForIndexing', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
provider.scheduleForIndexing.andCallThrough();
|
||||||
|
spyOn(provider, 'keepIndexing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks ids to index', function () {
|
||||||
|
expect(provider.indexedIds.a).not.toBeDefined();
|
||||||
|
expect(provider.pendingIndex.a).not.toBeDefined();
|
||||||
|
expect(provider.idsToIndex).not.toContain('a');
|
||||||
|
provider.scheduleForIndexing('a');
|
||||||
|
expect(provider.indexedIds.a).toBeDefined();
|
||||||
|
expect(provider.pendingIndex.a).toBeDefined();
|
||||||
|
expect(provider.idsToIndex).toContain('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls keep indexing', function () {
|
||||||
|
provider.scheduleForIndexing('a');
|
||||||
|
expect(provider.keepIndexing).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keepIndexing', function () {
|
||||||
|
it('calls beginIndexRequest until at maximum', function () {
|
||||||
|
spyOn(provider, 'beginIndexRequest').andCallThrough();
|
||||||
|
provider.pendingRequests = 9;
|
||||||
|
provider.idsToIndex = ['a', 'b', 'c'];
|
||||||
|
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||||
|
provider.keepIndexing();
|
||||||
|
expect(provider.beginIndexRequest).toHaveBeenCalled();
|
||||||
|
expect(provider.beginIndexRequest.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls beginIndexRequest for all ids to index', function () {
|
||||||
|
spyOn(provider, 'beginIndexRequest').andCallThrough();
|
||||||
|
provider.pendingRequests = 0;
|
||||||
|
provider.idsToIndex = ['a', 'b', 'c'];
|
||||||
|
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||||
|
provider.keepIndexing();
|
||||||
|
expect(provider.beginIndexRequest).toHaveBeenCalled();
|
||||||
|
expect(provider.beginIndexRequest.calls.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not index when at capacity', function () {
|
||||||
|
spyOn(provider, 'beginIndexRequest');
|
||||||
|
provider.pendingRequests = 10;
|
||||||
|
provider.idsToIndex.push('a');
|
||||||
|
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||||
|
provider.keepIndexing();
|
||||||
|
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not index when no ids to index', function () {
|
||||||
|
spyOn(provider, 'beginIndexRequest');
|
||||||
|
provider.pendingRequests = 0;
|
||||||
|
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||||
|
provider.keepIndexing();
|
||||||
|
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('index', function () {
|
||||||
|
it('sends index message to worker', function () {
|
||||||
|
var id = 'anId',
|
||||||
|
model = {};
|
||||||
|
|
||||||
|
provider.index(id, model);
|
||||||
|
expect(worker.postMessage).toHaveBeenCalledWith({
|
||||||
|
request: 'index',
|
||||||
|
id: id,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('schedules composed ids for indexing', function () {
|
||||||
|
var id = 'anId',
|
||||||
|
model = {composition: ['abc', 'def']};
|
||||||
|
|
||||||
|
provider.index(id, model);
|
||||||
|
expect(provider.scheduleForIndexing)
|
||||||
|
.toHaveBeenCalledWith('abc');
|
||||||
|
expect(provider.scheduleForIndexing)
|
||||||
|
.toHaveBeenCalledWith('def');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beginIndexRequest', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockQ = jasmine.createSpyObj(
|
provider.pendingRequests = 0;
|
||||||
"$q",
|
provider.pendingIds = {'abc': true};
|
||||||
[ "defer" ]
|
provider.idsToIndex = ['abc'];
|
||||||
);
|
models.abc = {};
|
||||||
mockLog = jasmine.createSpyObj(
|
spyOn(provider, 'index');
|
||||||
"$log",
|
|
||||||
[ "error", "warn", "info", "debug" ]
|
|
||||||
);
|
|
||||||
mockDeferred = jasmine.createSpyObj(
|
|
||||||
"deferred",
|
|
||||||
[ "resolve", "reject"]
|
|
||||||
);
|
|
||||||
mockDeferred.promise = "mock promise";
|
|
||||||
mockQ.defer.andReturn(mockDeferred);
|
|
||||||
|
|
||||||
mockThrottle = jasmine.createSpy("throttle");
|
|
||||||
mockThrottledFn = jasmine.createSpy("throttledFn");
|
|
||||||
throttledCallCount = 0;
|
|
||||||
|
|
||||||
mockObjectService = jasmine.createSpyObj(
|
|
||||||
"objectService",
|
|
||||||
[ "getObjects" ]
|
|
||||||
);
|
|
||||||
mockObjectPromise = jasmine.createSpyObj(
|
|
||||||
"promise",
|
|
||||||
[ "then", "catch" ]
|
|
||||||
);
|
|
||||||
mockChainedPromise = jasmine.createSpyObj(
|
|
||||||
"chainedPromise",
|
|
||||||
[ "then" ]
|
|
||||||
);
|
|
||||||
mockObjectService.getObjects.andReturn(mockObjectPromise);
|
|
||||||
|
|
||||||
mockTopic = jasmine.createSpy('topic');
|
|
||||||
|
|
||||||
mockWorkerService = jasmine.createSpyObj(
|
|
||||||
"workerService",
|
|
||||||
[ "run" ]
|
|
||||||
);
|
|
||||||
mockWorker = jasmine.createSpyObj(
|
|
||||||
"worker",
|
|
||||||
[ "postMessage" ]
|
|
||||||
);
|
|
||||||
mockWorkerService.run.andReturn(mockWorker);
|
|
||||||
|
|
||||||
mockCapabilityPromise = jasmine.createSpyObj(
|
|
||||||
"promise",
|
|
||||||
[ "then", "catch" ]
|
|
||||||
);
|
|
||||||
|
|
||||||
mockDomainObjects = {};
|
|
||||||
['a', 'root1', 'root2'].forEach(function (id) {
|
|
||||||
mockDomainObjects[id] = (
|
|
||||||
jasmine.createSpyObj(
|
|
||||||
"domainObject",
|
|
||||||
[
|
|
||||||
"getId",
|
|
||||||
"getModel",
|
|
||||||
"hasCapability",
|
|
||||||
"getCapability",
|
|
||||||
"useCapability"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
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.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 () {
|
it('removes items from queue', function () {
|
||||||
var i;
|
provider.beginIndexRequest();
|
||||||
|
expect(provider.idsToIndex.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
resolveThrottledFn();
|
it('tracks number of pending requests', function () {
|
||||||
|
provider.beginIndexRequest();
|
||||||
expect(mockObjectService.getObjects).toHaveBeenCalled();
|
expect(provider.pendingRequests).toBe(1);
|
||||||
expect(mockObjectPromise.then).toHaveBeenCalled();
|
waitsFor(function () {
|
||||||
|
return provider.pendingRequests === 0;
|
||||||
// Call through the root-getting part
|
});
|
||||||
resolveObjectPromises();
|
runs(function () {
|
||||||
|
expect(provider.pendingRequests).toBe(0);
|
||||||
mockRoots.forEach(function (id) {
|
|
||||||
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
||||||
request: 'index',
|
|
||||||
model: mockDomainObjects[id].getModel(),
|
|
||||||
id: id
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("indexes members of composition", function () {
|
it('indexes objects', function () {
|
||||||
mockDomainObjects.root1.getModel.andReturn({
|
provider.beginIndexRequest();
|
||||||
composition: ['a']
|
waitsFor(function () {
|
||||||
|
return provider.pendingRequests === 0;
|
||||||
});
|
});
|
||||||
|
runs(function () {
|
||||||
resolveAsyncTasks();
|
expect(provider.index)
|
||||||
resolveAsyncTasks();
|
.toHaveBeenCalledWith('abc', models.abc);
|
||||||
|
|
||||||
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);
|
|
||||||
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
||||||
request: "search",
|
|
||||||
input: ' test "query" ',
|
|
||||||
timestamp: timestamp,
|
|
||||||
maxNumber: 1,
|
|
||||||
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 = {
|
|
||||||
data: {
|
|
||||||
request: "search",
|
|
||||||
results: {
|
|
||||||
1: 1,
|
|
||||||
2: 2
|
|
||||||
},
|
|
||||||
total: 2,
|
|
||||||
timedOut: false,
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
it('can dispatch searches to worker', function () {
|
||||||
|
spyOn(provider, 'makeQueryId').andReturn(428);
|
||||||
|
expect(provider.dispatchSearch('searchTerm', 100))
|
||||||
|
.toBe(428);
|
||||||
|
|
||||||
|
expect(worker.postMessage).toHaveBeenCalledWith({
|
||||||
|
request: 'search',
|
||||||
|
input: 'searchTerm',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 428
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can generate queryIds', function () {
|
||||||
|
expect(provider.makeQueryId()).toEqual(jasmine.any(Number));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can query for terms', function () {
|
||||||
|
var deferred = {promise: {}};
|
||||||
|
spyOn(provider, 'dispatchSearch').andReturn(303);
|
||||||
|
$q.defer.andReturn(deferred);
|
||||||
|
|
||||||
|
expect(provider.query('someTerm', 100)).toBe(deferred.promise);
|
||||||
|
expect(provider.pendingQueries[303]).toBe(deferred);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onWorkerMessage', function () {
|
||||||
|
var pendingQuery;
|
||||||
|
beforeEach(function () {
|
||||||
|
pendingQuery = jasmine.createSpyObj(
|
||||||
|
'pendingQuery',
|
||||||
|
['resolve']
|
||||||
|
);
|
||||||
|
provider.pendingQueries[143] = pendingQuery;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves pending searches', function () {
|
||||||
|
provider.onWorkerMessage({
|
||||||
|
data: {
|
||||||
|
request: 'search',
|
||||||
|
total: 2,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
item: {
|
||||||
|
id: 'abc',
|
||||||
|
model: {id: 'abc'}
|
||||||
|
},
|
||||||
|
matchCount: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: {
|
||||||
|
id: 'def',
|
||||||
|
model: {id: 'def'}
|
||||||
|
},
|
||||||
|
matchCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
queryId: 143
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pendingQuery.resolve)
|
||||||
|
.toHaveBeenCalledWith({
|
||||||
|
total: 2,
|
||||||
|
hits: [{
|
||||||
|
id: 'abc',
|
||||||
|
model: {id: 'abc'},
|
||||||
|
score: 4
|
||||||
|
}, {
|
||||||
|
id: 'def',
|
||||||
|
model: {id: 'def'},
|
||||||
|
score: 2
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(provider.pendingQueries[143]).not.toBeDefined();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* Administration. All rights reserved.
|
* Administration. All rights reserved.
|
||||||
*
|
*
|
||||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
* 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.
|
* 'License'); you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
@ -19,114 +19,205 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,require*/
|
/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,
|
||||||
|
require,afterEach*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchSpec. Created by shale on 07/31/2015.
|
* SearchSpec. Created by shale on 07/31/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
describe("The generic search worker ", function () {
|
], function (
|
||||||
// If this test fails, make sure this path is correct
|
|
||||||
var worker = new Worker(require.toUrl('platform/search/src/services/GenericSearchWorker.js')),
|
|
||||||
numObjects = 5;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
) {
|
||||||
var i;
|
'use strict';
|
||||||
for (i = 0; i < numObjects; i += 1) {
|
|
||||||
worker.postMessage(
|
|
||||||
{
|
|
||||||
request: "index",
|
|
||||||
id: i,
|
|
||||||
model: {
|
|
||||||
name: "object " + i,
|
|
||||||
id: i,
|
|
||||||
type: "something"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("searches can reach all objects", function () {
|
describe('GenericSearchWorker', function () {
|
||||||
var flag = false,
|
// If this test fails, make sure this path is correct
|
||||||
workerOutput,
|
var worker,
|
||||||
resultsLength = 0;
|
objectX,
|
||||||
|
objectY,
|
||||||
|
objectZ,
|
||||||
|
itemsToIndex,
|
||||||
|
onMessage,
|
||||||
|
data,
|
||||||
|
waitForResult;
|
||||||
|
|
||||||
// Search something that should return all objects
|
beforeEach(function () {
|
||||||
runs(function () {
|
worker = new Worker(
|
||||||
worker.postMessage(
|
require.toUrl('platform/search/src/services/GenericSearchWorker.js')
|
||||||
{
|
);
|
||||||
request: "search",
|
|
||||||
input: "object",
|
|
||||||
maxNumber: 100,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
timeout: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
worker.onmessage = function (event) {
|
objectX = {
|
||||||
var id;
|
id: 'x',
|
||||||
|
model: {name: 'object xx'}
|
||||||
|
};
|
||||||
|
objectY = {
|
||||||
|
id: 'y',
|
||||||
|
model: {name: 'object yy'}
|
||||||
|
};
|
||||||
|
objectZ = {
|
||||||
|
id: 'z',
|
||||||
|
model: {name: 'object zz'}
|
||||||
|
};
|
||||||
|
itemsToIndex = [
|
||||||
|
objectX,
|
||||||
|
objectY,
|
||||||
|
objectZ
|
||||||
|
];
|
||||||
|
|
||||||
workerOutput = event.data;
|
itemsToIndex.forEach(function (item) {
|
||||||
for (id in workerOutput.results) {
|
worker.postMessage({
|
||||||
resultsLength += 1;
|
request: 'index',
|
||||||
}
|
id: item.id,
|
||||||
flag = true;
|
model: item.model
|
||||||
};
|
|
||||||
|
|
||||||
waitsFor(function () {
|
|
||||||
return flag;
|
|
||||||
}, "The worker should be searching", 1000);
|
|
||||||
|
|
||||||
runs(function () {
|
|
||||||
expect(workerOutput).toBeDefined();
|
|
||||||
expect(resultsLength).toEqual(numObjects);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("searches return only matches", function () {
|
onMessage = jasmine.createSpy('onMessage');
|
||||||
var flag = false,
|
worker.addEventListener('message', onMessage);
|
||||||
workerOutput,
|
|
||||||
resultsLength = 0;
|
|
||||||
|
|
||||||
// Search something that should return 1 object
|
|
||||||
runs(function () {
|
|
||||||
worker.postMessage(
|
|
||||||
{
|
|
||||||
request: "search",
|
|
||||||
input: "2",
|
|
||||||
maxNumber: 100,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
timeout: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
worker.onmessage = function (event) {
|
|
||||||
var id;
|
|
||||||
|
|
||||||
workerOutput = event.data;
|
|
||||||
for (id in workerOutput.results) {
|
|
||||||
resultsLength += 1;
|
|
||||||
}
|
|
||||||
flag = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
waitForResult = function () {
|
||||||
waitsFor(function () {
|
waitsFor(function () {
|
||||||
return flag;
|
if (onMessage.calls.length > 0) {
|
||||||
}, "The worker should be searching", 1000);
|
data = onMessage.calls[0].args[0].data;
|
||||||
|
return true;
|
||||||
runs(function () {
|
}
|
||||||
expect(workerOutput).toBeDefined();
|
return false;
|
||||||
expect(resultsLength).toEqual(1);
|
|
||||||
expect(workerOutput.results[2]).toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns search results for partial term matches', function () {
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'obj',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 123
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(onMessage).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(data.request).toBe('search');
|
||||||
|
expect(data.total).toBe(3);
|
||||||
|
expect(data.queryId).toBe(123);
|
||||||
|
expect(data.results.length).toBe(3);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].item.model).toEqual(objectX.model);
|
||||||
|
expect(data.results[0].matchCount).toBe(1);
|
||||||
|
expect(data.results[1].item.id).toBe('y');
|
||||||
|
expect(data.results[1].item.model).toEqual(objectY.model);
|
||||||
|
expect(data.results[1].matchCount).toBe(1);
|
||||||
|
expect(data.results[2].item.id).toBe('z');
|
||||||
|
expect(data.results[2].item.model).toEqual(objectZ.model);
|
||||||
|
expect(data.results[2].matchCount).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
it('scores exact term matches higher', function () {
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'object',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 234
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(data.queryId).toBe(234);
|
||||||
|
expect(data.results.length).toBe(3);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].matchCount).toBe(1.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can find partial term matches', function () {
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'x',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 345
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(data.queryId).toBe(345);
|
||||||
|
expect(data.results.length).toBe(1);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].matchCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches individual terms', function () {
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'x y z',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 456
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(data.queryId).toBe(456);
|
||||||
|
expect(data.results.length).toBe(3);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].matchCount).toBe(1);
|
||||||
|
expect(data.results[1].item.id).toBe('y');
|
||||||
|
expect(data.results[1].matchCount).toBe(1);
|
||||||
|
expect(data.results[2].item.id).toBe('z');
|
||||||
|
expect(data.results[1].matchCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scores exact matches highest', function () {
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'object xx',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 567
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(data.queryId).toBe(567);
|
||||||
|
expect(data.results.length).toBe(3);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].matchCount).toBe(103);
|
||||||
|
expect(data.results[1].matchCount).toBe(1.5);
|
||||||
|
expect(data.results[2].matchCount).toBe(1.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scores multiple term match above single match', function () {
|
||||||
|
worker.postMessage({
|
||||||
|
request: 'search',
|
||||||
|
input: 'obj x',
|
||||||
|
maxResults: 100,
|
||||||
|
queryId: 678
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForResult();
|
||||||
|
|
||||||
|
runs(function () {
|
||||||
|
expect(data.queryId).toBe(678);
|
||||||
|
expect(data.results.length).toBe(3);
|
||||||
|
expect(data.results[0].item.id).toBe('x');
|
||||||
|
expect(data.results[0].matchCount).toBe(2);
|
||||||
|
expect(data.results[1].matchCount).toBe(1);
|
||||||
|
expect(data.results[2].matchCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -19,83 +19,244 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
/*global define,describe,it,expect,beforeEach,jasmine,Promise,waitsFor,spyOn*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchSpec. Created by shale on 07/31/2015.
|
* SearchSpec. Created by shale on 07/31/2015.
|
||||||
*/
|
*/
|
||||||
define(
|
define([
|
||||||
["../../src/services/SearchAggregator"],
|
"../../src/services/SearchAggregator"
|
||||||
function (SearchAggregator) {
|
], function (SearchAggregator) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
describe("The search aggregator ", function () {
|
describe("SearchAggregator", function () {
|
||||||
var mockQ,
|
var $q,
|
||||||
mockPromise,
|
objectService,
|
||||||
mockProviders = [],
|
providers,
|
||||||
aggregator,
|
aggregator;
|
||||||
mockProviderResults = [],
|
|
||||||
mockAggregatorResults,
|
|
||||||
i;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockQ = jasmine.createSpyObj(
|
|
||||||
"$q",
|
|
||||||
[ "all" ]
|
|
||||||
);
|
|
||||||
mockPromise = jasmine.createSpyObj(
|
|
||||||
"promise",
|
|
||||||
[ "then" ]
|
|
||||||
);
|
|
||||||
for (i = 0; i < 3; i += 1) {
|
|
||||||
mockProviders.push(
|
|
||||||
jasmine.createSpyObj(
|
|
||||||
"mockProvider" + i,
|
|
||||||
[ "query" ]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
mockProviders[i].query.andReturn(mockPromise);
|
|
||||||
}
|
|
||||||
mockQ.all.andReturn(mockPromise);
|
|
||||||
|
|
||||||
aggregator = new SearchAggregator(mockQ, mockProviders);
|
|
||||||
aggregator.query();
|
|
||||||
|
|
||||||
for (i = 0; i < mockProviders.length; i += 1) {
|
|
||||||
mockProviderResults.push({
|
|
||||||
hits: [
|
|
||||||
{
|
|
||||||
id: i,
|
|
||||||
score: 42 - i
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: i + 1,
|
|
||||||
score: 42 - (2 * i)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
mockAggregatorResults = mockPromise.then.mostRecentCall.args[0](mockProviderResults);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends queries to all providers", function () {
|
|
||||||
for (i = 0; i < mockProviders.length; i += 1) {
|
|
||||||
expect(mockProviders[i].query).toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters out duplicate objects", function () {
|
|
||||||
expect(mockAggregatorResults.hits.length).toEqual(mockProviders.length + 1);
|
|
||||||
expect(mockAggregatorResults.total).not.toBeLessThan(mockAggregatorResults.hits.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("orders results by score", function () {
|
|
||||||
for (i = 1; i < mockAggregatorResults.hits.length; i += 1) {
|
|
||||||
expect(mockAggregatorResults.hits[i].score)
|
|
||||||
.not.toBeGreaterThan(mockAggregatorResults.hits[i - 1].score);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$q = jasmine.createSpyObj(
|
||||||
|
'$q',
|
||||||
|
['all']
|
||||||
|
);
|
||||||
|
$q.all.andReturn(Promise.resolve([]));
|
||||||
|
objectService = jasmine.createSpyObj(
|
||||||
|
'objectService',
|
||||||
|
['getObjects']
|
||||||
|
);
|
||||||
|
providers = [];
|
||||||
|
aggregator = new SearchAggregator($q, objectService, providers);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
it("has a fudge factor", function () {
|
||||||
|
expect(aggregator.FUDGE_FACTOR).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has default max results", function () {
|
||||||
|
expect(aggregator.DEFAULT_MAX_RESULTS).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can order model results by score", function () {
|
||||||
|
var modelResults = {
|
||||||
|
hits: [
|
||||||
|
{score: 1},
|
||||||
|
{score: 23},
|
||||||
|
{score: 11}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sorted = aggregator.orderByScore(modelResults);
|
||||||
|
|
||||||
|
expect(sorted.hits).toEqual([
|
||||||
|
{score: 23},
|
||||||
|
{score: 11},
|
||||||
|
{score: 1}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters results without a function', function () {
|
||||||
|
var modelResults = {
|
||||||
|
hits: [
|
||||||
|
{thing: 1},
|
||||||
|
{thing: 2}
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
},
|
||||||
|
filtered = aggregator.applyFilter(modelResults);
|
||||||
|
|
||||||
|
expect(filtered.hits).toEqual([
|
||||||
|
{thing: 1},
|
||||||
|
{thing: 2}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filtered.total).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters results with a function', function () {
|
||||||
|
var modelResults = {
|
||||||
|
hits: [
|
||||||
|
{model: {thing: 1}},
|
||||||
|
{model: {thing: 2}},
|
||||||
|
{model: {thing: 3}}
|
||||||
|
],
|
||||||
|
total: 3
|
||||||
|
},
|
||||||
|
filterFunc = function (model) {
|
||||||
|
return model.thing < 2;
|
||||||
|
},
|
||||||
|
filtered = aggregator.applyFilter(modelResults, filterFunc);
|
||||||
|
|
||||||
|
expect(filtered.hits).toEqual([
|
||||||
|
{model: {thing: 1}}
|
||||||
|
]);
|
||||||
|
expect(filtered.total).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can remove duplicates', function () {
|
||||||
|
var modelResults = {
|
||||||
|
hits: [
|
||||||
|
{id: 15},
|
||||||
|
{id: 23},
|
||||||
|
{id: 14},
|
||||||
|
{id: 23}
|
||||||
|
],
|
||||||
|
total: 4
|
||||||
|
},
|
||||||
|
deduped = aggregator.removeDuplicates(modelResults);
|
||||||
|
|
||||||
|
expect(deduped.hits).toEqual([
|
||||||
|
{id: 15},
|
||||||
|
{id: 23},
|
||||||
|
{id: 14}
|
||||||
|
]);
|
||||||
|
expect(deduped.total).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can convert model results to object results', function () {
|
||||||
|
var modelResults = {
|
||||||
|
hits: [
|
||||||
|
{id: 123, score: 5},
|
||||||
|
{id: 234, score: 1}
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
},
|
||||||
|
objects = {
|
||||||
|
123: '123-object-hey',
|
||||||
|
234: '234-object-hello'
|
||||||
|
},
|
||||||
|
promiseChainComplete = false;
|
||||||
|
|
||||||
|
objectService.getObjects.andReturn(Promise.resolve(objects));
|
||||||
|
|
||||||
|
aggregator
|
||||||
|
.asObjectResults(modelResults)
|
||||||
|
.then(function (objectResults) {
|
||||||
|
expect(objectResults).toEqual({
|
||||||
|
hits: [
|
||||||
|
{id: 123, score: 5, object: '123-object-hey'},
|
||||||
|
{id: 234, score: 1, object: '234-object-hello'}
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
promiseChainComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
waitsFor(function () {
|
||||||
|
return promiseChainComplete;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can send queries to providers', function () {
|
||||||
|
var provider = jasmine.createSpyObj(
|
||||||
|
'provider',
|
||||||
|
['query']
|
||||||
|
);
|
||||||
|
provider.query.andReturn('i prooomise!');
|
||||||
|
providers.push(provider);
|
||||||
|
|
||||||
|
aggregator.query('find me', 123, 'filter');
|
||||||
|
expect(provider.query)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
'find me',
|
||||||
|
123 * aggregator.FUDGE_FACTOR
|
||||||
|
);
|
||||||
|
expect($q.all).toHaveBeenCalledWith(['i prooomise!']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supplies max results when none is provided', function () {
|
||||||
|
var provider = jasmine.createSpyObj(
|
||||||
|
'provider',
|
||||||
|
['query']
|
||||||
|
);
|
||||||
|
providers.push(provider);
|
||||||
|
aggregator.query('find me');
|
||||||
|
expect(provider.query).toHaveBeenCalledWith(
|
||||||
|
'find me',
|
||||||
|
aggregator.DEFAULT_MAX_RESULTS * aggregator.FUDGE_FACTOR
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can combine responses from multiple providers', function () {
|
||||||
|
var providerResponses = [
|
||||||
|
{
|
||||||
|
hits: [
|
||||||
|
'oneHit',
|
||||||
|
'twoHit'
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hits: [
|
||||||
|
'redHit',
|
||||||
|
'blueHit',
|
||||||
|
'by',
|
||||||
|
'Pete'
|
||||||
|
],
|
||||||
|
total: 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
promiseChainResolved = false;
|
||||||
|
|
||||||
|
$q.all.andReturn(Promise.resolve(providerResponses));
|
||||||
|
spyOn(aggregator, 'orderByScore').andReturn('orderedByScore!');
|
||||||
|
spyOn(aggregator, 'applyFilter').andReturn('filterApplied!');
|
||||||
|
spyOn(aggregator, 'removeDuplicates')
|
||||||
|
.andReturn('duplicatesRemoved!');
|
||||||
|
spyOn(aggregator, 'asObjectResults').andReturn('objectResults');
|
||||||
|
|
||||||
|
aggregator
|
||||||
|
.query('something', 10, 'filter')
|
||||||
|
.then(function (objectResults) {
|
||||||
|
expect(aggregator.orderByScore).toHaveBeenCalledWith({
|
||||||
|
hits: [
|
||||||
|
'oneHit',
|
||||||
|
'twoHit',
|
||||||
|
'redHit',
|
||||||
|
'blueHit',
|
||||||
|
'by',
|
||||||
|
'Pete'
|
||||||
|
],
|
||||||
|
total: 6
|
||||||
|
});
|
||||||
|
expect(aggregator.applyFilter)
|
||||||
|
.toHaveBeenCalledWith('orderedByScore!', 'filter');
|
||||||
|
expect(aggregator.removeDuplicates)
|
||||||
|
.toHaveBeenCalledWith('filterApplied!');
|
||||||
|
expect(aggregator.asObjectResults)
|
||||||
|
.toHaveBeenCalledWith('duplicatesRemoved!');
|
||||||
|
|
||||||
|
expect(objectResults).toBe('objectResults');
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
promiseChainResolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
waitsFor(function () {
|
||||||
|
return promiseChainResolved;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -44,7 +44,8 @@ require.config({
|
|||||||
|
|
||||||
paths: {
|
paths: {
|
||||||
'es6-promise': 'platform/framework/lib/es6-promise-2.0.0.min',
|
'es6-promise': 'platform/framework/lib/es6-promise-2.0.0.min',
|
||||||
'moment-duration-format': 'warp/clock/lib/moment-duration-format'
|
'moment-duration-format': 'warp/clock/lib/moment-duration-format',
|
||||||
|
'uuid': 'platform/commonUI/browse/lib/uuid'
|
||||||
},
|
},
|
||||||
|
|
||||||
// dynamically load all test files
|
// dynamically load all test files
|
||||||
|
Reference in New Issue
Block a user