mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 09:26:45 +00:00
Merging in latest github/master
open #90 Squashed commit of the following: commit a2d06583ca9f8b5a5d5db4e53bf8b522d173d49c 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 commit 5d5425db04b5fe668f37c1a5afe1534852b1b638 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; commit a8856c061247b3edc758b4961dbe2325bc4b62ae 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) commit 74f289cb34a9ad1f396590c8288d56b0a45b90ef 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 commit 4ec243c6fbf2096d4241670f87cc1b41034b9d77 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 commit 407d9881ff6260865abce146c512a4468f6cc402 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 commit 6ee622b3f5bbe65b5d75967e01e7b1791dcb42cb 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 commit 099d70b8d93c3d77e28fe9a5045b5eb2597df056 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 commit 3d996ac46696b983addd8c4696d5fee983eaf1a2 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. commit 90828ef63d712921672c431bfb310a41c7d0d35d 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 commit 29bdc9d57407c1aa7cb7a8549b0f4e70edf9dd2f 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. commit bf24ac7c933fb14cb9bd108e9b1b50070ac8a507 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. commit 59f094763b5993f208a52fcc50daf999e2f4198b 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 commit dbebf085007be053a6f67d326b127cef16c93913 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. commit 847c3560630430cba3ef1a8bf0339280222097ce 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 commit 06bcd28558f5e9f6b3fd0e83c60a705a35702846 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 commit f88e8ebb51feed773e95206674cbb83da142e139 Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Wed Oct 21 15:08:44 2015 -0700 [Time Controller] Update model state for text entry commit 6d2b2fd81e0423faf7cfa0ea20bf55d30dd5e586 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. commit 608800ae637cb4bc92f66fa8a5f9788751a7e9cb 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 commit 07818b0a6d69e521ee962a5e8c0351d08153ffc1 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. commit 496cf85b7e8b55f5b432444bd63058e0b0a93d5a Author: Pete Richards <peter.l.richards@nasa.gov> Date: Wed Oct 21 09:46:32 2015 -0700 [JSDoc] Correct mistake commit 833f57e284d3f68cb70dd49ab7e27f20c7610c4c 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. commit 9a63e997108000278b973c2128afc91309bac2b8 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. commit 21739fffd91c4892d427502e299550e49bb2e12a 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. commit 77d81f899bf2c1fb91d03bfc24399d8ba0f505a8 Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:31:33 2015 -0700 [Style] JSLint compliance commit fe3263fdfe8a1af779a37a4664ddd30c4e2380f4 Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 15:27:46 2015 -0700 [Search] Remove invalid specs commit ce42429fbdd23bc5eb52cb542e0f3c1be87398f0 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. commit 76151d09a0a589fb646494079ebf8a483ecb2d11 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. commit ec7e6cc5b445439630ce56121383f47eb8849cde Author: Pete Richards <peter.l.richards@nasa.gov> Date: Tue Oct 20 13:55:46 2015 -0700 [Search] Update spec for Generic Search Worker commit 1ddce48f7ec6b58627c7fea3e66cf8b2d5d94e6b 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. commit 98b5ff3c77bb6fe88b6bcbd13d0db840e7bc33df Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 18:14:33 2015 -0700 [Search] Decrement number of pending requests commit 14094a48fc542eca54d02f6db84cd4233305ffa5 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. commit 8e2a2eeba5db5c9c14769c75bfe67fcfb63491a9 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 commit 0f63e4dde9673c7edd85c0ca187a74a4add1fe53 Author: Pete Richards <peter.l.richards@nasa.gov> Date: Fri Oct 16 17:06:23 2015 -0700 [Tests] Rewrite search aggregator specs commit 12efb47be74bc52ca67a7600e588c08ad8c67590 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. commit a2fce8e56c824c458fb625155abc92c086438c08 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. commit 78e5c0143b67c48c00adb40702e54829839eeda0 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. commit 099591ad2eb427ecde5d3dbde11a5c0c52b974c7 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. commit b5505f372f6a737a2a00a1f987aee97ee5facecf 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. commit 9ad860babdc95a49fbbbd1a50f3e16e3c7802f6e 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. commit 87e317a6f5b4f4e0eeabfc9268699784764dec6e 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. commit bf41d82a78282fdac3acaed1845188bf69eed651 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 commit a4944717a1eb1bcbe385dd3ddc7321ea8805ab34 Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:47:37 2015 -0700 [Location] Test getOriginal method commit 70bbd3cf97f43914572df00783763b208cbf08fa Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:37:37 2015 -0700 [Entanglement] Add test cases for Go To Original commit e3afaf0842afa5abb3f3991f65a20f39c937f0a5 Author: Victor Woeltjen <victor.woeltjen@nasa.gov> Date: Tue Oct 6 16:22:16 2015 -0700 [Entanglement] Add Go To Original nasa/openmctweb#147 commit 60f2f9fb6c50e19348f264cb3aae4f3baeea15a5 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:
parent
daed6a5b06
commit
0a19ab4389
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/identity",
|
||||
"platform/persistence/local",
|
||||
"platform/persistence/queue",
|
||||
"platform/persistence/elastic",
|
||||
"platform/policy",
|
||||
"platform/entanglement",
|
||||
"platform/search",
|
||||
|
@ -30,7 +30,8 @@
|
||||
var CONSTANTS = {
|
||||
DIAGRAM_WIDTH: 800,
|
||||
DIAGRAM_HEIGHT: 500
|
||||
};
|
||||
},
|
||||
TOC_HEAD = "# Table of Contents";
|
||||
|
||||
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
|
||||
(function () {
|
||||
@ -44,6 +45,7 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
||||
split = require("split"),
|
||||
stream = require("stream"),
|
||||
nomnoml = require('nomnoml'),
|
||||
toc = require("markdown-toc"),
|
||||
Canvas = require('canvas'),
|
||||
options = require("minimist")(process.argv.slice(2));
|
||||
|
||||
@ -110,6 +112,9 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
||||
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(marked(markdown));
|
||||
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) {
|
||||
// ...but only if they look like relative paths
|
||||
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
|
||||
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
||||
renderer.link.apply(renderer, arguments);
|
||||
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
||||
renderer.link.apply(renderer, arguments);
|
||||
};
|
||||
return customRenderer;
|
||||
}
|
||||
|
@ -30,25 +30,25 @@ define(
|
||||
YELLOW = 0.5,
|
||||
LIMITS = {
|
||||
rh: {
|
||||
cssClass: "s-limit-upr-red",
|
||||
cssClass: "s-limit-upr s-limit-red",
|
||||
low: RED,
|
||||
high: Number.POSITIVE_INFINITY,
|
||||
name: "Red High"
|
||||
},
|
||||
rl: {
|
||||
cssClass: "s-limit-lwr-red",
|
||||
cssClass: "s-limit-lwr s-limit-red",
|
||||
high: -RED,
|
||||
low: Number.NEGATIVE_INFINITY,
|
||||
name: "Red Low"
|
||||
},
|
||||
yh: {
|
||||
cssClass: "s-limit-upr-yellow",
|
||||
cssClass: "s-limit-upr s-limit-yellow",
|
||||
low: YELLOW,
|
||||
high: RED,
|
||||
name: "Yellow High"
|
||||
},
|
||||
yl: {
|
||||
cssClass: "s-limit-lwr-yellow",
|
||||
cssClass: "s-limit-lwr s-limit-yellow",
|
||||
low: -RED,
|
||||
high: -YELLOW,
|
||||
name: "Yellow Low"
|
||||
|
@ -22,7 +22,8 @@
|
||||
"split": "^1.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nomnoml": "^0.0.3",
|
||||
"canvas": "^1.2.7"
|
||||
"canvas": "^1.2.7",
|
||||
"markdown-toc": "^0.11.7"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
|
@ -1,4 +1,9 @@
|
||||
{
|
||||
"configuration": {
|
||||
"paths": {
|
||||
"uuid": "uuid"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"routes": [
|
||||
{
|
||||
|
@ -25,7 +25,7 @@
|
||||
* Module defining CreateService. Created by vwoeltje on 11/10/14.
|
||||
*/
|
||||
define(
|
||||
["../../lib/uuid"],
|
||||
["uuid"],
|
||||
function (uuid) {
|
||||
"use strict";
|
||||
|
||||
|
@ -110,3 +110,8 @@ $dirImgs: $dirCommonRes + 'images/';
|
||||
|
||||
/************************** TIMINGS */
|
||||
$controlFadeMs: 100ms;
|
||||
|
||||
/************************** LIMITS */
|
||||
$glyphLimit: '\e603';
|
||||
$glyphLimitUpr: '\0000eb';
|
||||
$glyphLimitLwr: '\0000ee';
|
||||
|
@ -1,26 +1,39 @@
|
||||
@mixin limit($bg, $ic, $glyph) {
|
||||
background: $bg !important;
|
||||
//color: $fg !important;
|
||||
&:before {
|
||||
//@include pulse(1000ms);
|
||||
color: $ic;
|
||||
content: $glyph;
|
||||
}
|
||||
@mixin limitGlyph($iconColor, $glyph: $glyphLimit) {
|
||||
&:before {
|
||||
color: $iconColor;
|
||||
content: $glyph;
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.8em;
|
||||
display: inline;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[class*="s-limit"] {
|
||||
//white-space: nowrap;
|
||||
&:before {
|
||||
display: inline-block;
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.75em;
|
||||
font-style: normal !important;
|
||||
margin-right: $interiorMarginSm;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.s-limit-red { background: $colorLimitRedBg !important; }
|
||||
.s-limit-yellow { background: $colorLimitYellowBg !important; }
|
||||
|
||||
// Handle limit when applied to a tr
|
||||
tr[class*="s-limit"] {
|
||||
&.s-limit-red td:first-child {
|
||||
@include limitGlyph($colorLimitRedIc);
|
||||
}
|
||||
&.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"); };
|
||||
.s-limit-upr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ed"); };
|
||||
.s-limit-lwr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ec"); };
|
||||
.s-limit-lwr-red { @include limit($colorLimitRedBg, $colorLimitRedIc, "\0000ee"); };
|
||||
// Handle limit when applied directly to a non-tr element
|
||||
// Assume this is applied to the element that displays the limit value
|
||||
:not(tr)[class*="s-limit"] {
|
||||
&.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
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<!-- MINE -->
|
||||
<div ng-controller="TimeRangeController">
|
||||
<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-input" ng-controller="ToggleController as t1">
|
||||
<!--<span class="lbl">Start</span>-->
|
||||
<span class="s-btn time-range-start" ng-click="t1.toggle()">
|
||||
<span class="val">{{startOuterText}}</span>
|
||||
<a class="ui-symbol icon icon-calendar"></a>
|
||||
<mct-popup ng-if="t1.isActive()">
|
||||
<div mct-click-elsewhere="t1.setState(false)">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel.outer"
|
||||
field="'start'"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
</span>
|
||||
</span>
|
||||
<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-input" ng-controller="ToggleController as t1">
|
||||
<!--<span class="lbl">Start</span>-->
|
||||
<span class="s-btn time-range-start">
|
||||
<input type="text"
|
||||
ng-model="boundsModel.start"
|
||||
ng-class="{ error: !boundsModel.startValid }">
|
||||
</input>
|
||||
<a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
|
||||
<mct-popup ng-if="t1.isActive()">
|
||||
<div mct-click-elsewhere="t1.setState(false)">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel.outer"
|
||||
field="'start'"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<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="lbl">End</span>-->
|
||||
<span class="s-btn l-time-range-input" ng-click="t2.toggle()">
|
||||
<span class="val">{{endOuterText}}</span>
|
||||
<a class="ui-symbol icon icon-calendar"></a>
|
||||
<mct-popup ng-if="t2.isActive()">
|
||||
<div mct-click-elsewhere="t2.setState(false)">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel.outer"
|
||||
field="'end'"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
||||
<!--<span class="lbl">End</span>-->
|
||||
<span class="s-btn l-time-range-input">
|
||||
<input type="text"
|
||||
ng-model="boundsModel.end"
|
||||
ng-class="{ error: !boundsModel.endValid }">
|
||||
</input>
|
||||
<a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
|
||||
</a>
|
||||
<mct-popup ng-if="t2.isActive()">
|
||||
<div mct-click-elsewhere="t2.setState(false)">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel.outer"
|
||||
field="'end'"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="l-time-range-slider-holder">
|
||||
<div class="l-time-range-slider">
|
||||
<div class="slider"
|
||||
mct-resize="spanWidth = bounds.width">
|
||||
<div class="knob knob-l"
|
||||
mct-drag-down="startLeftDrag()"
|
||||
mct-drag="leftDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct }">
|
||||
<div class="range-value">{{startInnerText}}</div>
|
||||
</div>
|
||||
<div class="knob knob-r"
|
||||
mct-drag-down="startRightDrag()"
|
||||
mct-drag="rightDrag(delta[0])"
|
||||
ng-style="{ right: endInnerPct }">
|
||||
<div class="range-value">{{endInnerText}}</div>
|
||||
</div>
|
||||
<div class="slot range-holder">
|
||||
<div class="range"
|
||||
mct-drag-down="startMiddleDrag()"
|
||||
mct-drag="middleDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct, right: endInnerPct}">
|
||||
<div class="toi-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="l-time-range-slider-holder">
|
||||
<div class="l-time-range-slider">
|
||||
<div class="slider"
|
||||
mct-resize="spanWidth = bounds.width">
|
||||
<div class="knob knob-l"
|
||||
mct-drag-down="startLeftDrag()"
|
||||
mct-drag="leftDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct }">
|
||||
<div class="range-value">{{startInnerText}}</div>
|
||||
</div>
|
||||
<div class="knob knob-r"
|
||||
mct-drag-down="startRightDrag()"
|
||||
mct-drag="rightDrag(delta[0])"
|
||||
ng-style="{ right: endInnerPct }">
|
||||
<div class="range-value">{{endInnerText}}</div>
|
||||
</div>
|
||||
<div class="slot range-holder">
|
||||
<div class="range"
|
||||
mct-drag-down="startMiddleDrag()"
|
||||
mct-drag="middleDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct, right: endInnerPct}">
|
||||
<div class="toi-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="l-time-range-ticks-holder">
|
||||
<div class="l-time-range-ticks">
|
||||
<div
|
||||
ng-repeat="tick in ticks"
|
||||
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
|
||||
class="tick tick-x"
|
||||
>
|
||||
<span class="l-time-range-tick-label">{{tick}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="l-time-range-ticks-holder">
|
||||
<div class="l-time-range-ticks">
|
||||
<div
|
||||
ng-repeat="tick in ticks"
|
||||
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
|
||||
class="tick tick-x"
|
||||
>
|
||||
<span class="l-time-range-tick-label">{{tick}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,9 +26,8 @@ define(
|
||||
function (moment) {
|
||||
"use strict";
|
||||
|
||||
var
|
||||
DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
||||
TICK_SPACING_PX = 150;
|
||||
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
||||
TICK_SPACING_PX = 150;
|
||||
|
||||
/**
|
||||
* @memberof platform/commonUI/general
|
||||
@ -44,6 +43,15 @@ define(
|
||||
return moment.utc(ts).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
function parseTimestamp(text) {
|
||||
var m = moment.utc(text, DATE_FORMAT);
|
||||
if (m.isValid()) {
|
||||
return m.valueOf();
|
||||
} else {
|
||||
throw new Error("Could not parse " + text);
|
||||
}
|
||||
}
|
||||
|
||||
// From 0.0-1.0 to "0%"-"1%"
|
||||
function toPercent(p) {
|
||||
return (100 * p) + "%";
|
||||
@ -93,6 +101,25 @@ define(
|
||||
return { start: bounds.start, end: bounds.end };
|
||||
}
|
||||
|
||||
function updateBoundsTextForProperty(ngModel, property) {
|
||||
try {
|
||||
if (!$scope.boundsModel[property] ||
|
||||
parseTimestamp($scope.boundsModel[property]) !==
|
||||
ngModel.outer[property]) {
|
||||
$scope.boundsModel[property] =
|
||||
formatTimestamp(ngModel.outer[property]);
|
||||
}
|
||||
} catch (e) {
|
||||
// User-entered text is invalid, so leave it be
|
||||
// until they fix it.
|
||||
}
|
||||
}
|
||||
|
||||
function updateBoundsText(ngModel) {
|
||||
updateBoundsTextForProperty(ngModel, 'start');
|
||||
updateBoundsTextForProperty(ngModel, 'end');
|
||||
}
|
||||
|
||||
function updateViewFromModel(ngModel) {
|
||||
var t = now();
|
||||
|
||||
@ -101,8 +128,7 @@ define(
|
||||
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
|
||||
|
||||
// First, dates for the date pickers for outer bounds
|
||||
$scope.startOuterDate = new Date(ngModel.outer.start);
|
||||
$scope.endOuterDate = new Date(ngModel.outer.end);
|
||||
updateBoundsText(ngModel);
|
||||
|
||||
// Then various updates for the inner span
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
@ -178,6 +204,8 @@ define(
|
||||
function updateOuterStart(t) {
|
||||
var ngModel = $scope.ngModel;
|
||||
|
||||
ngModel.outer.start = t;
|
||||
|
||||
ngModel.outer.end = Math.max(
|
||||
ngModel.outer.start + outerMinimumSpan,
|
||||
ngModel.outer.end
|
||||
@ -190,14 +218,15 @@ define(
|
||||
ngModel.inner.end
|
||||
);
|
||||
|
||||
$scope.startOuterText = formatTimestamp(t);
|
||||
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateOuterEnd(t) {
|
||||
var ngModel = $scope.ngModel;
|
||||
|
||||
ngModel.outer.end = t;
|
||||
|
||||
ngModel.outer.start = Math.min(
|
||||
ngModel.outer.end - outerMinimumSpan,
|
||||
ngModel.outer.start
|
||||
@ -210,9 +239,40 @@ define(
|
||||
ngModel.inner.start
|
||||
);
|
||||
|
||||
$scope.endOuterText = formatTimestamp(t);
|
||||
|
||||
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;
|
||||
@ -224,14 +284,17 @@ define(
|
||||
|
||||
$scope.state = false;
|
||||
$scope.ticks = [];
|
||||
$scope.boundsModel = {};
|
||||
|
||||
// Initialize scope to defaults
|
||||
updateViewFromModel($scope.ngModel);
|
||||
|
||||
$scope.$watchCollection("ngModel", updateViewFromModel);
|
||||
$scope.$watch("spanWidth", updateSpanWidth);
|
||||
$scope.$watch("ngModel.outer.start", updateOuterStart);
|
||||
$scope.$watch("ngModel.outer.end", updateOuterEnd);
|
||||
$scope.$watch("ngModel.outer.start", updateStartFromPicker);
|
||||
$scope.$watch("ngModel.outer.end", updateEndFromPicker);
|
||||
$scope.$watch("boundsModel.start", updateStartFromText);
|
||||
$scope.$watch("boundsModel.end", updateEndFromText);
|
||||
}
|
||||
|
||||
return TimeConductorController;
|
||||
|
@ -22,8 +22,8 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/controllers/TimeRangeController"],
|
||||
function (TimeRangeController) {
|
||||
["../../src/controllers/TimeRangeController", "moment"],
|
||||
function (TimeRangeController, moment) {
|
||||
"use strict";
|
||||
|
||||
var SEC = 1000,
|
||||
@ -166,8 +166,72 @@ define(
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toBeGreaterThan(mockScope.ngModel.inner.start);
|
||||
});
|
||||
|
||||
describe("by typing", function () {
|
||||
it("updates models", function () {
|
||||
var newStart = "1977-05-25 17:30:00",
|
||||
newEnd = "2015-12-18 03:30:00";
|
||||
|
||||
mockScope.boundsModel.start = newStart;
|
||||
fireWatch("boundsModel.start", newStart);
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(moment.utc(newStart).valueOf());
|
||||
expect(mockScope.boundsModel.startValid)
|
||||
.toBeTruthy();
|
||||
|
||||
mockScope.boundsModel.end = newEnd;
|
||||
fireWatch("boundsModel.end", newEnd);
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.toEqual(moment.utc(newEnd).valueOf());
|
||||
expect(mockScope.boundsModel.endValid)
|
||||
.toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays error state", function () {
|
||||
var newStart = "Not a date",
|
||||
newEnd = "Definitely not a date",
|
||||
oldStart = mockScope.ngModel.outer.start,
|
||||
oldEnd = mockScope.ngModel.outer.end;
|
||||
|
||||
mockScope.boundsModel.start = newStart;
|
||||
fireWatch("boundsModel.start", newStart);
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(oldStart);
|
||||
expect(mockScope.boundsModel.startValid)
|
||||
.toBeFalsy();
|
||||
|
||||
mockScope.boundsModel.end = newEnd;
|
||||
fireWatch("boundsModel.end", newEnd);
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.toEqual(oldEnd);
|
||||
expect(mockScope.boundsModel.endValid)
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not modify user input", function () {
|
||||
// Don't want the controller "fixing" bad or
|
||||
// irregularly-formatted input out from under
|
||||
// the user's fingertips.
|
||||
var newStart = "Not a date",
|
||||
newEnd = "2015-3-3 01:02:04",
|
||||
oldStart = mockScope.ngModel.outer.start,
|
||||
oldEnd = mockScope.ngModel.outer.end;
|
||||
|
||||
mockScope.boundsModel.start = newStart;
|
||||
fireWatch("boundsModel.start", newStart);
|
||||
expect(mockScope.boundsModel.start)
|
||||
.toEqual(newStart);
|
||||
|
||||
mockScope.boundsModel.end = newEnd;
|
||||
fireWatch("boundsModel.end", newEnd);
|
||||
expect(mockScope.boundsModel.end)
|
||||
.toEqual(newEnd);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -147,6 +147,7 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
|
||||
/************************** CONTROLS */
|
||||
/************************** PATHS */
|
||||
/************************** TIMINGS */
|
||||
/************************** LIMITS */
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@ -667,45 +668,58 @@ mct-container {
|
||||
content: "!"; }
|
||||
|
||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||
[class*="s-limit"]:before {
|
||||
display: inline-block;
|
||||
.s-limit-red {
|
||||
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-size: 0.75em;
|
||||
font-style: normal !important;
|
||||
margin-right: 3px;
|
||||
vertical-align: middle; }
|
||||
|
||||
/* line 23, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-red:before {
|
||||
color: red;
|
||||
content: "ë"; }
|
||||
|
||||
font-size: 0.8em;
|
||||
display: inline;
|
||||
margin-right: 3px; }
|
||||
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-yellow td:first-child:before {
|
||||
color: #ffaa00;
|
||||
content: "";
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.8em;
|
||||
display: inline;
|
||||
margin-right: 3px; }
|
||||
/* line 24, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-yellow {
|
||||
background: rgba(255, 170, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-yellow:before {
|
||||
color: #ffaa00;
|
||||
content: "í"; }
|
||||
|
||||
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||
content: "ë"; }
|
||||
/* line 25, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-yellow {
|
||||
background: rgba(255, 170, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-yellow:before {
|
||||
color: #ffaa00;
|
||||
content: "ì"; }
|
||||
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||
content: "î"; }
|
||||
|
||||
/* line 26, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-red:before {
|
||||
color: red;
|
||||
content: "î"; }
|
||||
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||
:not(tr)[class*="s-limit"].s-limit-red:before {
|
||||
color: red;
|
||||
content: "";
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.8em;
|
||||
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 */
|
||||
.s-stale {
|
||||
@ -4275,13 +4289,45 @@ span.req {
|
||||
|
||||
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.pane-tree-hidden .pane.left.treeview {
|
||||
right: 100% !important;
|
||||
width: auto !important;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden; }
|
||||
-moz-transition-property: opacity;
|
||||
-o-transition-property: opacity;
|
||||
-webkit-transition-property: opacity;
|
||||
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 {
|
||||
-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-size: 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%);
|
||||
right: auto !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 {
|
||||
left: 40% !important; }
|
||||
|
||||
/* line 93, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.toggle-tree {
|
||||
color: #0099cc !important;
|
||||
font-size: 110%;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 10px; }
|
||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.toggle-tree:after {
|
||||
content: 'm' !important;
|
||||
font-family: symbolsfont; }
|
||||
|
||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar {
|
||||
left: 30px !important; }
|
||||
/* line 108, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 114, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar .context-available {
|
||||
opacity: 1 !important; }
|
||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 117, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar .view-switcher {
|
||||
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 {
|
||||
display: none; }
|
||||
|
||||
/* line 120, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 126, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.tree-holder {
|
||||
overflow-x: hidden !important; }
|
||||
|
||||
/* line 124, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 130, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-disable-select {
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
-webkit-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-important {
|
||||
display: none !important; }
|
||||
|
||||
/* line 134, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 140, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-back-hide {
|
||||
pointer-events: none;
|
||||
-moz-transition-property: opacity;
|
||||
@ -4355,7 +4401,7 @@ span.req {
|
||||
transition-delay: 0;
|
||||
opacity: 0; }
|
||||
|
||||
/* line 139, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 145, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-back-unhide {
|
||||
pointer-events: all;
|
||||
-moz-transition-property: opacity;
|
||||
@ -4376,21 +4422,21 @@ span.req {
|
||||
transition-delay: 0;
|
||||
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) {
|
||||
/* line 148, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.pane-tree-showing .pane.left.treeview {
|
||||
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 {
|
||||
left: 0 !important;
|
||||
-moz-transform: translateX(90%);
|
||||
-ms-transform: translateX(90%);
|
||||
-webkit-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 {
|
||||
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) {
|
||||
/* line 162, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 168, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.desktop-hide {
|
||||
display: none; } }
|
||||
/*****************************************************************************
|
||||
|
@ -147,6 +147,7 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
|
||||
/************************** CONTROLS */
|
||||
/************************** PATHS */
|
||||
/************************** TIMINGS */
|
||||
/************************** LIMITS */
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@ -667,45 +668,58 @@ mct-container {
|
||||
content: "!"; }
|
||||
|
||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||
[class*="s-limit"]:before {
|
||||
display: inline-block;
|
||||
.s-limit-red {
|
||||
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-size: 0.75em;
|
||||
font-style: normal !important;
|
||||
margin-right: 3px;
|
||||
vertical-align: middle; }
|
||||
|
||||
/* line 23, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-red:before {
|
||||
color: red;
|
||||
content: "ë"; }
|
||||
|
||||
font-size: 0.8em;
|
||||
display: inline;
|
||||
margin-right: 3px; }
|
||||
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-yellow td:first-child:before {
|
||||
color: #ffaa00;
|
||||
content: "";
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.8em;
|
||||
display: inline;
|
||||
margin-right: 3px; }
|
||||
/* line 24, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-yellow {
|
||||
background: rgba(255, 170, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-upr-yellow:before {
|
||||
color: #ffaa00;
|
||||
content: "í"; }
|
||||
|
||||
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||
content: "ë"; }
|
||||
/* line 25, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-yellow {
|
||||
background: rgba(255, 170, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-yellow:before {
|
||||
color: #ffaa00;
|
||||
content: "ì"; }
|
||||
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||
content: "î"; }
|
||||
|
||||
/* line 26, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
/* line 4, ../../../../general/res/sass/_limits.scss */
|
||||
.s-limit-lwr-red:before {
|
||||
color: red;
|
||||
content: "î"; }
|
||||
/* line 2, ../../../../general/res/sass/_limits.scss */
|
||||
:not(tr)[class*="s-limit"].s-limit-red:before {
|
||||
color: red;
|
||||
content: "";
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.8em;
|
||||
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 */
|
||||
.s-stale {
|
||||
@ -4216,13 +4230,45 @@ span.req {
|
||||
|
||||
/* line 65, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.pane-tree-hidden .pane.left.treeview {
|
||||
right: 100% !important;
|
||||
width: auto !important;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden; }
|
||||
-moz-transition-property: opacity;
|
||||
-o-transition-property: opacity;
|
||||
-webkit-transition-property: opacity;
|
||||
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 {
|
||||
-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-size: 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%);
|
||||
right: auto !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 {
|
||||
left: 40% !important; }
|
||||
|
||||
/* line 93, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.toggle-tree {
|
||||
color: #0099cc !important;
|
||||
font-size: 110%;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 10px; }
|
||||
/* line 99, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.toggle-tree:after {
|
||||
content: 'm' !important;
|
||||
font-family: symbolsfont; }
|
||||
|
||||
/* line 105, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar {
|
||||
left: 30px !important; }
|
||||
/* line 108, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 114, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar .context-available {
|
||||
opacity: 1 !important; }
|
||||
/* line 111, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 117, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.object-browse-bar .view-switcher {
|
||||
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 {
|
||||
display: none; }
|
||||
|
||||
/* line 120, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 126, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.tree-holder {
|
||||
overflow-x: hidden !important; }
|
||||
|
||||
/* line 124, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 130, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-disable-select {
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
-webkit-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-important {
|
||||
display: none !important; }
|
||||
|
||||
/* line 134, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 140, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-back-hide {
|
||||
pointer-events: none;
|
||||
-moz-transition-property: opacity;
|
||||
@ -4296,7 +4342,7 @@ span.req {
|
||||
transition-delay: 0;
|
||||
opacity: 0; }
|
||||
|
||||
/* line 139, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 145, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.mobile-back-unhide {
|
||||
pointer-events: all;
|
||||
-moz-transition-property: opacity;
|
||||
@ -4317,21 +4363,21 @@ span.req {
|
||||
transition-delay: 0;
|
||||
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) {
|
||||
/* line 148, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 154, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.pane-tree-showing .pane.left.treeview {
|
||||
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 {
|
||||
left: 0 !important;
|
||||
-moz-transform: translateX(90%);
|
||||
-ms-transform: translateX(90%);
|
||||
-webkit-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 {
|
||||
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) {
|
||||
/* line 162, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
/* line 168, ../../../../general/res/sass/mobile/_layout.scss */
|
||||
.desktop-hide {
|
||||
display: none; } }
|
||||
/*****************************************************************************
|
||||
|
@ -30,6 +30,14 @@
|
||||
"category": "contextual",
|
||||
"implementation": "actions/LinkAction.js",
|
||||
"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": [
|
||||
@ -52,7 +60,8 @@
|
||||
"key": "location",
|
||||
"name": "Location Capability",
|
||||
"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": [
|
||||
|
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 */
|
||||
|
||||
define(
|
||||
@ -12,11 +34,41 @@ define(
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function LocationCapability(domainObject) {
|
||||
function LocationCapability($q, $injector, domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.$q = $q;
|
||||
this.$injector = $injector;
|
||||
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
|
||||
* object.
|
||||
@ -78,10 +130,6 @@ define(
|
||||
return !this.isLink();
|
||||
};
|
||||
|
||||
function createLocationCapability(domainObject) {
|
||||
return new LocationCapability(domainObject);
|
||||
}
|
||||
|
||||
return createLocationCapability;
|
||||
return LocationCapability;
|
||||
}
|
||||
);
|
||||
|
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 */
|
||||
|
||||
define(
|
||||
@ -7,6 +29,7 @@ define(
|
||||
'../ControlledPromise'
|
||||
],
|
||||
function (LocationCapability, domainObjectFactory, ControlledPromise) {
|
||||
'use strict';
|
||||
|
||||
describe("LocationCapability", function () {
|
||||
|
||||
@ -14,13 +37,17 @@ define(
|
||||
var locationCapability,
|
||||
persistencePromise,
|
||||
mutationPromise,
|
||||
mockQ,
|
||||
mockInjector,
|
||||
mockObjectService,
|
||||
domainObject;
|
||||
|
||||
beforeEach(function () {
|
||||
domainObject = domainObjectFactory({
|
||||
id: "testObject",
|
||||
capabilities: {
|
||||
context: {
|
||||
getParent: function() {
|
||||
getParent: function () {
|
||||
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();
|
||||
domainObject.capabilities.persistence.persist.andReturn(
|
||||
persistencePromise
|
||||
@ -49,7 +81,11 @@ define(
|
||||
}
|
||||
);
|
||||
|
||||
locationCapability = new LocationCapability(domainObject);
|
||||
locationCapability = new LocationCapability(
|
||||
mockQ,
|
||||
mockInjector,
|
||||
domainObject
|
||||
);
|
||||
});
|
||||
|
||||
it("returns contextual location", function () {
|
||||
@ -88,6 +124,57 @@ define(
|
||||
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/CopyAction",
|
||||
"actions/GoToOriginalAction",
|
||||
"actions/LinkAction",
|
||||
"actions/MoveAction",
|
||||
"services/CopyService",
|
||||
"services/LinkService",
|
||||
"services/MoveService",
|
||||
|
@ -159,7 +159,9 @@ define(
|
||||
|
||||
// Update dimensions and origin based on extrema of plots
|
||||
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],
|
||||
priorDomainDimensions = this.dimensions[0];
|
||||
|
||||
|
@ -202,6 +202,38 @@ define(
|
||||
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",
|
||||
"type": "provider",
|
||||
"implementation": "ElasticSearchProvider.js",
|
||||
"depends": [ "$http", "objectService", "ELASTIC_ROOT" ]
|
||||
"depends": [ "$http", "ELASTIC_ROOT" ]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
|
@ -24,190 +24,122 @@
|
||||
/**
|
||||
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
define([
|
||||
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var ID = "_id",
|
||||
SCORE = "_score",
|
||||
DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* 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;
|
||||
}
|
||||
], function (
|
||||
|
||||
/**
|
||||
* Searches through the filetree for domain objects using a search
|
||||
* term. This is done through querying elasticsearch. Returns a
|
||||
* 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) {
|
||||
editDistance = '';
|
||||
}
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
return searchTerm.split(' ').map(function (s) {
|
||||
// Don't add fuzziness for quoted strings
|
||||
if (s.indexOf('"') !== -1) {
|
||||
return s;
|
||||
} else {
|
||||
return s + '~' + editDistance;
|
||||
}
|
||||
}).join(' ');
|
||||
}
|
||||
var ID_PROPERTY = '_id',
|
||||
SOURCE_PROPERTY = '_source',
|
||||
SCORE_PROPERTY = '_score';
|
||||
|
||||
// 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;
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree using ElasticSearch.
|
||||
*
|
||||
* @constructor
|
||||
* @param $http Angular's $http service, for working with urls.
|
||||
* @param ROOT the constant `ELASTIC_ROOT` which allows us to
|
||||
* interact with ElasticSearch.
|
||||
*/
|
||||
function ElasticSearchProvider($http, ROOT) {
|
||||
this.$http = $http;
|
||||
this.root = ROOT;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
define(
|
||||
["../src/ElasticSearchProvider"],
|
||||
function (ElasticSearchProvider) {
|
||||
"use strict";
|
||||
define([
|
||||
'../src/ElasticSearchProvider'
|
||||
], function (
|
||||
ElasticSearchProvider
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var ID = "_id",
|
||||
SCORE = "_score";
|
||||
|
||||
describe("The ElasticSearch search provider ", function () {
|
||||
var mockHttp,
|
||||
mockHttpPromise,
|
||||
mockObjectPromise,
|
||||
mockObjectService,
|
||||
mockDomainObject,
|
||||
provider,
|
||||
mockProviderResults;
|
||||
describe('ElasticSearchProvider', function () {
|
||||
var $http,
|
||||
ROOT,
|
||||
provider;
|
||||
|
||||
beforeEach(function () {
|
||||
$http = jasmine.createSpy('$http');
|
||||
ROOT = 'http://localhost:9200';
|
||||
|
||||
provider = new ElasticSearchProvider($http, ROOT);
|
||||
});
|
||||
|
||||
describe('query', function () {
|
||||
beforeEach(function () {
|
||||
mockHttp = jasmine.createSpy("$http");
|
||||
mockHttpPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then" ]
|
||||
);
|
||||
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);
|
||||
spyOn(provider, 'cleanTerm').andReturn('cleanedTerm');
|
||||
spyOn(provider, 'fuzzyMatchUnquotedTerms').andReturn('fuzzy');
|
||||
spyOn(provider, 'parseResponse').andReturn('parsedResponse');
|
||||
$http.andReturn(Promise.resolve({}));
|
||||
});
|
||||
|
||||
it("sends a query to ElasticSearch", function () {
|
||||
expect(mockHttp).toHaveBeenCalled();
|
||||
|
||||
it('cleans terms and adds fuzzyness', function () {
|
||||
provider.query('hello', 10);
|
||||
expect(provider.cleanTerm).toHaveBeenCalledWith('hello');
|
||||
expect(provider.fuzzyMatchUnquotedTerms)
|
||||
.toHaveBeenCalledWith('cleanedTerm');
|
||||
});
|
||||
|
||||
it("gets data from ElasticSearch", function () {
|
||||
var data = {
|
||||
hits: {
|
||||
hits: [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
total: 0
|
||||
|
||||
it('calls through to $http', function () {
|
||||
provider.query('hello', 10);
|
||||
expect($http).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
params: {
|
||||
q: 'fuzzy',
|
||||
size: 10
|
||||
},
|
||||
timed_out: false
|
||||
};
|
||||
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);
|
||||
url: 'http://localhost:9200/_search/'
|
||||
});
|
||||
});
|
||||
|
||||
it("returns nothing for an empty string query", function () {
|
||||
expect(provider.query("").hits).toEqual([]);
|
||||
|
||||
it('gracefully fails when http fails', function () {
|
||||
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 () {
|
||||
mockProviderResults = mockHttpPromise.then.mostRecentCall.args[1]();
|
||||
expect(mockProviderResults).toBeDefined();
|
||||
|
||||
it('parses and returns when http succeeds', function () {
|
||||
var promiseChainResolved = false;
|
||||
$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": [
|
||||
"$q",
|
||||
"$log",
|
||||
"throttle",
|
||||
"objectService",
|
||||
"modelService",
|
||||
"workerService",
|
||||
"topic",
|
||||
"GENERIC_SEARCH_ROOTS"
|
||||
@ -59,7 +58,7 @@
|
||||
"provides": "searchService",
|
||||
"type": "aggregator",
|
||||
"implementation": "services/SearchAggregator.js",
|
||||
"depends": [ "$q" ]
|
||||
"depends": [ "$q", "objectService" ]
|
||||
}
|
||||
],
|
||||
"workers": [
|
||||
|
@ -21,21 +21,16 @@
|
||||
-->
|
||||
<div class="search"
|
||||
ng-controller="SearchController as controller">
|
||||
|
||||
|
||||
<!-- Search bar -->
|
||||
<div class="search-bar"
|
||||
ng-controller="ClickAwayController as toggle">
|
||||
|
||||
|
||||
<!-- Input field -->
|
||||
<input class="search-input"
|
||||
type="text"
|
||||
ng-model="ngModel.input"
|
||||
ng-keyup="controller.search()" />
|
||||
<!--mct-control key="'textfield'"
|
||||
class="search-input"
|
||||
ng-model="ngModel.input"
|
||||
ng-keyup="controller.search()">
|
||||
</mct-control-->
|
||||
|
||||
<!-- Search icon -->
|
||||
<!-- ui symbols for search are 'd' and 'M' -->
|
||||
@ -43,20 +38,20 @@
|
||||
ng-class="{content: !(ngModel.input === '' || ngModel.input === undefined)}">
|
||||
M
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Clear icon/button 'x' -->
|
||||
<a class="ui-symbol clear-icon"
|
||||
ng-class="{content: !(ngModel.input === '' || ngModel.input === undefined)}"
|
||||
ng-click="ngModel.input = ''; controller.search()">
|
||||

|
||||
</a>
|
||||
|
||||
|
||||
<!-- Menu icon/button 'v' -->
|
||||
<a class="ui-symbol menu-icon"
|
||||
ng-click="toggle.toggle()">
|
||||
v
|
||||
</a>
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<mct-representation key="'search-menu'"
|
||||
class="menu-element search-menu-holder"
|
||||
@ -65,27 +60,24 @@
|
||||
ng-click="toggle.setState(true)">
|
||||
</mct-representation>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Active filter display -->
|
||||
<div class="active-filter-display"
|
||||
ng-class="{off: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}"
|
||||
ng-controller="SearchMenuController as menuController">
|
||||
|
||||
|
||||
<a class="ui-symbol clear-filters-icon"
|
||||
ng-click="ngModel.checkAll = true; menuController.checkAll()">
|
||||

|
||||
</a>
|
||||
|
||||
Filtered by: {{ ngModel.filtersString }}
|
||||
|
||||
<!--div class="filter-options">
|
||||
Filtered by: {{ ngModel.filtersString }}
|
||||
</div-->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- This div exists to determine scroll bar location -->
|
||||
<div class="search-scroll abs">
|
||||
|
||||
|
||||
<!-- Results list -->
|
||||
<div class="results">
|
||||
<mct-representation key="'search-item'"
|
||||
@ -103,14 +95,14 @@
|
||||
<span class="title-label">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- Load more button -->
|
||||
<!-- Load more button -->
|
||||
<div ng-if="controller.areMore()">
|
||||
<a class="load-more-button s-btn vsm"
|
||||
ng-click="controller.loadMore()">
|
||||
ng-click="controller.loadMore()">
|
||||
More Results
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -26,146 +26,155 @@
|
||||
*/
|
||||
define(function () {
|
||||
"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) {
|
||||
// numResults is the amount of results to display. Will get increased.
|
||||
// fullResults holds the most recent complete searchService response object
|
||||
var numResults = INITIAL_LOAD_NUMBER,
|
||||
fullResults = {hits: []};
|
||||
|
||||
// Scope variables are:
|
||||
// Variables used only in SearchController:
|
||||
// results, an array of searchResult objects
|
||||
// loading, whether search() is loading
|
||||
// ngModel.input, the text of the search query
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
var controller = this;
|
||||
this.$scope = $scope;
|
||||
this.searchService = searchService;
|
||||
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||
this.availabileResults = 0;
|
||||
this.$scope.results = [];
|
||||
this.$scope.loading = false;
|
||||
this.pendingQuery = undefined;
|
||||
this.$scope.ngModel.filter = function () {
|
||||
return controller.onFilterChange.apply(controller, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -19,234 +19,262 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
/*global define,setTimeout*/
|
||||
|
||||
/**
|
||||
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
define([
|
||||
|
||||
var DEFAULT_MAX_RESULTS = 100,
|
||||
DEFAULT_TIMEOUT = 1000,
|
||||
MAX_CONCURRENT_REQUESTS = 100,
|
||||
FLUSH_INTERVAL = 0,
|
||||
stopTime;
|
||||
], function (
|
||||
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree without using external search implementations.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param $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;
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
this.worker = worker;
|
||||
this.pendingQueries = pendingQueries;
|
||||
this.$q = $q;
|
||||
// pendingQueries is a dictionary with the key value pairs st
|
||||
// the key is the timestamp and the value is the promise
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree without using external search implementations.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param $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) {
|
||||
ids.forEach(function (id) {
|
||||
if (!indexed[id] && !pendingIndex[id]) {
|
||||
indexed[id] = true;
|
||||
pendingIndex[id] = true;
|
||||
toRequest.push(id);
|
||||
}
|
||||
});
|
||||
scheduleFlush();
|
||||
}
|
||||
this.indexedIds = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
|
||||
// Tell the web worker to add a domain object's model to its list of items.
|
||||
function indexItem(domainObject) {
|
||||
var model = domainObject.getModel();
|
||||
this.pendingQueries = {};
|
||||
|
||||
worker.postMessage({
|
||||
request: 'index',
|
||||
model: model,
|
||||
id: domainObject.getId()
|
||||
});
|
||||
this.worker = this.startWorker(workerService);
|
||||
this.indexOnMutation(topic);
|
||||
|
||||
if (Array.isArray(model.composition)) {
|
||||
scheduleIdsForIndexing(model.composition);
|
||||
}
|
||||
}
|
||||
ROOTS.forEach(function indexRoot(rootId) {
|
||||
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) {
|
||||
searchResults.push({
|
||||
object: objects[id],
|
||||
id: id,
|
||||
score: event.data.results[id]
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Maximum number of concurrent index requests to allow.
|
||||
*/
|
||||
GenericSearchProvider.prototype.MAX_CONCURRENT_REQUESTS = 100;
|
||||
|
||||
// Resove the promise corresponding to this
|
||||
pendingQueries[event.data.timestamp].resolve({
|
||||
hits: searchResults,
|
||||
total: event.data.total,
|
||||
timedOut: event.data.timedOut
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Query the search provider for results.
|
||||
*
|
||||
* @param {String} input the string to search by.
|
||||
* @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) {
|
||||
pendingRequests += 1;
|
||||
objectService.getObjects([id]).then(function (objects) {
|
||||
delete pendingIndex[id];
|
||||
if (objects[id]) {
|
||||
indexItem(objects[id]);
|
||||
}
|
||||
}, function () {
|
||||
$log.warn("Failed to index domain object " + id);
|
||||
}).then(function () {
|
||||
pendingRequests -= 1;
|
||||
scheduleFlush();
|
||||
});
|
||||
}
|
||||
var queryId = this.dispatchSearch(input, maxResults),
|
||||
pendingQuery = this.$q.defer();
|
||||
|
||||
scheduleFlush = throttle(function flush() {
|
||||
var batchSize =
|
||||
Math.max(MAX_CONCURRENT_REQUESTS - pendingRequests, 0);
|
||||
this.pendingQueries[queryId] = pendingQuery;
|
||||
|
||||
if (toRequest.length + pendingRequests < 1) {
|
||||
$log.info([
|
||||
'GenericSearch finished indexing after ',
|
||||
((Date.now() - indexingStarted) / 1000).toFixed(2),
|
||||
' seconds.'
|
||||
].join(''));
|
||||
} else {
|
||||
toRequest.splice(-batchSize, batchSize)
|
||||
.forEach(requestAndIndex);
|
||||
}
|
||||
}, FLUSH_INTERVAL);
|
||||
return pendingQuery.promise;
|
||||
};
|
||||
|
||||
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
|
||||
scheduleIdsForIndexing(ROOTS);
|
||||
worker.addEventListener('message', function (messageEvent) {
|
||||
provider.onWorkerMessage(messageEvent);
|
||||
});
|
||||
|
||||
// Re-index items when they are mutated
|
||||
mutationTopic.listen(function (domainObject) {
|
||||
var id = domainObject.getId();
|
||||
indexed[id] = false;
|
||||
scheduleIdsForIndexing([id]);
|
||||
return worker;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the search term. This function is to be used as a fallback
|
||||
* in the case where other search services are not avaliable.
|
||||
* Returns a 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 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();
|
||||
/**
|
||||
* Pulls an id from the indexing queue, loads it from the model service,
|
||||
* and indexes it. Upon completion, tells the provider to keep
|
||||
* indexing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
GenericSearchProvider.prototype.beginIndexRequest = function () {
|
||||
var idToIndex = this.idsToIndex.shift(),
|
||||
provider = this;
|
||||
|
||||
// Tell the worker to search for items it has that match this searchInput.
|
||||
// Takes the searchInput, as well as a max number of results (will return
|
||||
// less than that if there are fewer matches).
|
||||
function workerSearch(searchInput, maxResults, timestamp, timeout) {
|
||||
var message = {
|
||||
request: 'search',
|
||||
input: searchInput,
|
||||
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;
|
||||
this.pendingRequests += 1;
|
||||
this.modelService
|
||||
.getModels([idToIndex])
|
||||
.then(function (models) {
|
||||
delete provider.pendingIndex[idToIndex];
|
||||
if (models[idToIndex]) {
|
||||
provider.index(idToIndex, models[idToIndex]);
|
||||
}
|
||||
}, 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;
|
||||
} else {
|
||||
// Otherwise return an empty result
|
||||
return { hits: [], total: 0 };
|
||||
}
|
||||
};
|
||||
var pendingQuery = this.pendingQueries[event.data.queryId],
|
||||
modelResults = {
|
||||
total: event.data.total
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -26,133 +26,132 @@
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
|
||||
// An array of objects composed of domain object IDs and models
|
||||
// {id: domainObject's ID, model: domainObject's model}
|
||||
var indexedItems = [];
|
||||
|
||||
// Helper function for serach()
|
||||
function convertToTerms(input) {
|
||||
var terms = input;
|
||||
// Shave any spaces off of the ends of the input
|
||||
while (terms.substr(0, 1) === ' ') {
|
||||
terms = terms.substring(1, terms.length);
|
||||
}
|
||||
while (terms.substr(terms.length - 1, 1) === ' ') {
|
||||
terms = terms.substring(0, terms.length - 1);
|
||||
}
|
||||
|
||||
// Then split it at spaces and asterisks
|
||||
terms = terms.split(/ |\*/);
|
||||
|
||||
// Remove any empty strings from the terms
|
||||
while (terms.indexOf('') !== -1) {
|
||||
terms.splice(terms.indexOf(''), 1);
|
||||
}
|
||||
|
||||
return terms;
|
||||
var indexedItems = [],
|
||||
TERM_SPLITTER = /[ _\*]/;
|
||||
|
||||
function indexItem(id, model) {
|
||||
var vector = {
|
||||
name: model.name
|
||||
};
|
||||
vector.cleanName = model.name.trim();
|
||||
vector.lowerCaseName = vector.cleanName.toLocaleLowerCase();
|
||||
vector.terms = vector.lowerCaseName.split(TERM_SPLITTER);
|
||||
|
||||
indexedItems.push({
|
||||
id: id,
|
||||
vector: vector,
|
||||
model: model
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Helper function for search()
|
||||
function scoreItem(item, input, terms) {
|
||||
var name = item.model.name.toLocaleLowerCase(),
|
||||
weight = 0.65,
|
||||
score = 0.0,
|
||||
i;
|
||||
|
||||
// Make the score really big if the item name and
|
||||
// the original search input are the same
|
||||
if (name === input) {
|
||||
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;
|
||||
function convertToTerms(input) {
|
||||
var query = {
|
||||
exactInput: input
|
||||
};
|
||||
query.inputClean = input.trim();
|
||||
query.inputLowerCase = query.inputClean.toLocaleLowerCase();
|
||||
query.terms = query.inputLowerCase.split(TERM_SPLITTER);
|
||||
query.exactTerms = query.inputClean.split(TERM_SPLITTER);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Gets search results from the indexedItems based on provided search
|
||||
* input. Returns matching results from indexedItems, as well as the
|
||||
* timestamp that was passed to it.
|
||||
*
|
||||
* input. Returns matching results from indexedItems
|
||||
*
|
||||
* @param data An object which contains:
|
||||
* * input: The original string which we are searching with
|
||||
* * maxNumber: The maximum number of search results desired
|
||||
* * timestamp: The time identifier from when the query was made
|
||||
* * maxResults: The maximum number of search results desired
|
||||
* * queryId: an id identifying this query, will be returned.
|
||||
*/
|
||||
function search(data) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
var results = {},
|
||||
input = data.input.toLocaleLowerCase(),
|
||||
terms = convertToTerms(input),
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
var results,
|
||||
input = data.input,
|
||||
query = convertToTerms(input),
|
||||
message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
total: 0,
|
||||
timestamp: data.timestamp,
|
||||
timedOut: false
|
||||
queryId: data.queryId
|
||||
},
|
||||
score,
|
||||
i,
|
||||
id;
|
||||
|
||||
// If the user input is empty, we want to have no search results.
|
||||
if (input !== '') {
|
||||
for (i = 0; i < indexedItems.length; i += 1) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
matches = {};
|
||||
|
||||
if (!query.inputClean) {
|
||||
// No search terms, no results;
|
||||
return message;
|
||||
}
|
||||
|
||||
// Truncate results if there are more than maxResults
|
||||
if (message.total > data.maxResults) {
|
||||
i = 0;
|
||||
for (id in results) {
|
||||
message.results[id] = results[id];
|
||||
i += 1;
|
||||
if (i >= data.maxResults) {
|
||||
break;
|
||||
|
||||
// Two phases: find matches, then score matches.
|
||||
// Idea being that match finding should be fast, so that future scoring
|
||||
// operations process fewer objects.
|
||||
|
||||
query.terms.forEach(function findMatchingItems(term) {
|
||||
indexedItems
|
||||
.filter(function matchesItem(item) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// TODO: This seems inefficient.
|
||||
} else {
|
||||
message.results = results;
|
||||
}
|
||||
|
||||
return match;
|
||||
})
|
||||
.map(function prioritizeCompleteTermMatches(match) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
self.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
indexedItems.push({
|
||||
id: event.data.id,
|
||||
model: event.data.model
|
||||
});
|
||||
indexItem(event.data.id, event.data.model);
|
||||
} else if (event.data.request === 'search') {
|
||||
self.postMessage(search(event.data));
|
||||
}
|
||||
};
|
||||
}());
|
||||
}());
|
||||
|
@ -24,122 +24,201 @@
|
||||
/**
|
||||
* Module defining SearchAggregator. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
define([
|
||||
|
||||
var DEFUALT_TIMEOUT = 1000,
|
||||
DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* Allows multiple services which provide search functionality
|
||||
* to be treated as one.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param {SearchProvider[]} providers The search providers to be
|
||||
* aggregated.
|
||||
*/
|
||||
function SearchAggregator($q, providers) {
|
||||
this.$q = $q;
|
||||
this.providers = providers;
|
||||
], function (
|
||||
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Aggregates multiple search providers as a singular search provider.
|
||||
* Search providers are expected to implement a `query` method which returns
|
||||
* a promise for a `modelResults` object.
|
||||
*
|
||||
* The search aggregator combines the results from multiple providers,
|
||||
* removes aggregates, and converts the results to domain objects.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a query to each of the providers. Returns a 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}
|
||||
*
|
||||
* @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 = [];
|
||||
resultPromises = this.providers.map(function (provider) {
|
||||
return provider.query(
|
||||
inputText,
|
||||
maxResults * aggregator.FUDGE_FACTOR
|
||||
);
|
||||
});
|
||||
|
||||
// Remove duplicate objects that have the same ID. Modifies the passed
|
||||
// array, and returns the number that were removed.
|
||||
function filterDuplicates(results, total) {
|
||||
var ids = {},
|
||||
numRemoved = 0,
|
||||
i;
|
||||
return this.$q
|
||||
.all(resultPromises)
|
||||
.then(function (providerResults) {
|
||||
var modelResults = {
|
||||
hits: [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
for (i = 0; i < results.length; i += 1) {
|
||||
if (ids[results[i].id]) {
|
||||
// If this result's ID is already there, remove the object
|
||||
results.splice(i, 1);
|
||||
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;
|
||||
}
|
||||
providerResults.forEach(function (providerResult) {
|
||||
modelResults.hits =
|
||||
modelResults.hits.concat(providerResult.hits);
|
||||
modelResults.total += providerResult.total;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!maxResults) {
|
||||
maxResults = DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
modelResults = aggregator.orderByScore(modelResults);
|
||||
modelResults = aggregator.applyFilter(modelResults, filter);
|
||||
modelResults = aggregator.removeDuplicates(modelResults);
|
||||
|
||||
// Send the query to all the providers
|
||||
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 aggregator.asObjectResults(modelResults);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
@ -24,185 +24,162 @@
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
["../../src/controllers/SearchController"],
|
||||
function (SearchController) {
|
||||
"use strict";
|
||||
define([
|
||||
'../../src/controllers/SearchController'
|
||||
], function (
|
||||
SearchController
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
// These should be the same as the ones on the top of the search controller
|
||||
var INITIAL_LOAD_NUMBER = 20,
|
||||
LOAD_INCREMENT = 20;
|
||||
|
||||
describe("The search controller", function () {
|
||||
var mockScope,
|
||||
mockSearchService,
|
||||
mockPromise,
|
||||
mockSearchResult,
|
||||
mockDomainObject,
|
||||
mockTypes,
|
||||
controller;
|
||||
describe('The search controller', function () {
|
||||
var mockScope,
|
||||
mockSearchService,
|
||||
mockPromise,
|
||||
mockSearchResult,
|
||||
mockDomainObject,
|
||||
mockTypes,
|
||||
controller;
|
||||
|
||||
function bigArray(size) {
|
||||
var array = [],
|
||||
i;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
array.push(mockSearchResult);
|
||||
}
|
||||
return array;
|
||||
function bigArray(size) {
|
||||
var array = [],
|
||||
i;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
array.push(mockSearchResult);
|
||||
}
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch" ]
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.input = "test input";
|
||||
mockScope.ngModel.checked = {};
|
||||
mockScope.ngModel.checked['mock.type'] = true;
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
[ '$watch' ]
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.input = 'test input';
|
||||
mockScope.ngModel.checked = {};
|
||||
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;
|
||||
|
||||
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("sends queries to the search service", 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();
|
||||
controller.onFilterChange();
|
||||
var filterFn = mockSearchService.query.mostRecentCall.args[2];
|
||||
expect(filterFn('askbfa')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
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 () {
|
||||
it('returns true only for matching checked types', function () {
|
||||
mockScope.ngModel.checkAll = false;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + 5),
|
||||
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();
|
||||
controller.onFilterChange();
|
||||
var filterFn = mockSearchService.query.mostRecentCall.args[2];
|
||||
expect(filterFn({type: 'mock.type'})).toBe(true);
|
||||
expect(filterFn({type: 'other.type'})).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
define(
|
||||
["../../src/services/GenericSearchProvider"],
|
||||
function (GenericSearchProvider) {
|
||||
"use strict";
|
||||
define([
|
||||
"../../src/services/GenericSearchProvider"
|
||||
], function (
|
||||
GenericSearchProvider
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
describe("The generic search provider ", function () {
|
||||
var mockQ,
|
||||
mockLog,
|
||||
mockThrottle,
|
||||
mockDeferred,
|
||||
mockObjectService,
|
||||
mockObjectPromise,
|
||||
mockChainedPromise,
|
||||
mockDomainObjects,
|
||||
mockCapability,
|
||||
mockCapabilityPromise,
|
||||
mockWorkerService,
|
||||
mockWorker,
|
||||
mockTopic,
|
||||
mockMutationTopic,
|
||||
mockRoots = ['root1', 'root2'],
|
||||
mockThrottledFn,
|
||||
throttledCallCount,
|
||||
provider,
|
||||
mockProviderResults;
|
||||
describe('GenericSearchProvider', function () {
|
||||
var $q,
|
||||
$log,
|
||||
modelService,
|
||||
models,
|
||||
workerService,
|
||||
worker,
|
||||
topic,
|
||||
mutationTopic,
|
||||
ROOTS,
|
||||
provider;
|
||||
|
||||
function resolveObjectPromises() {
|
||||
var i;
|
||||
for (i = 0; i < mockObjectPromise.then.calls.length; i += 1) {
|
||||
mockChainedPromise.then.calls[i].args[0](
|
||||
mockObjectPromise.then.calls[i]
|
||||
.args[0](mockDomainObjects)
|
||||
);
|
||||
}
|
||||
}
|
||||
beforeEach(function () {
|
||||
$q = jasmine.createSpyObj(
|
||||
'$q',
|
||||
['defer']
|
||||
);
|
||||
$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() {
|
||||
if (mockThrottledFn.calls.length > throttledCallCount) {
|
||||
mockThrottle.mostRecentCall.args[0]();
|
||||
throttledCallCount = mockThrottledFn.calls.length;
|
||||
}
|
||||
}
|
||||
spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');
|
||||
|
||||
function resolveAsyncTasks() {
|
||||
resolveThrottledFn();
|
||||
resolveObjectPromises();
|
||||
}
|
||||
provider = new GenericSearchProvider(
|
||||
$q,
|
||||
$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 () {
|
||||
mockQ = jasmine.createSpyObj(
|
||||
"$q",
|
||||
[ "defer" ]
|
||||
);
|
||||
mockLog = jasmine.createSpyObj(
|
||||
"$log",
|
||||
[ "error", "warn", "info", "debug" ]
|
||||
);
|
||||
mockDeferred = jasmine.createSpyObj(
|
||||
"deferred",
|
||||
[ "resolve", "reject"]
|
||||
);
|
||||
mockDeferred.promise = "mock promise";
|
||||
mockQ.defer.andReturn(mockDeferred);
|
||||
|
||||
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
|
||||
);
|
||||
provider.pendingRequests = 0;
|
||||
provider.pendingIds = {'abc': true};
|
||||
provider.idsToIndex = ['abc'];
|
||||
models.abc = {};
|
||||
spyOn(provider, 'index');
|
||||
});
|
||||
|
||||
it("indexes tree on initialization", function () {
|
||||
var i;
|
||||
it('removes items from queue', function () {
|
||||
provider.beginIndexRequest();
|
||||
expect(provider.idsToIndex.length).toBe(0);
|
||||
});
|
||||
|
||||
resolveThrottledFn();
|
||||
|
||||
expect(mockObjectService.getObjects).toHaveBeenCalled();
|
||||
expect(mockObjectPromise.then).toHaveBeenCalled();
|
||||
|
||||
// Call through the root-getting part
|
||||
resolveObjectPromises();
|
||||
|
||||
mockRoots.forEach(function (id) {
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
||||
request: 'index',
|
||||
model: mockDomainObjects[id].getModel(),
|
||||
id: id
|
||||
});
|
||||
it('tracks number of pending requests', function () {
|
||||
provider.beginIndexRequest();
|
||||
expect(provider.pendingRequests).toBe(1);
|
||||
waitsFor(function () {
|
||||
return provider.pendingRequests === 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(provider.pendingRequests).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("indexes members of composition", function () {
|
||||
mockDomainObjects.root1.getModel.andReturn({
|
||||
composition: ['a']
|
||||
it('indexes objects', function () {
|
||||
provider.beginIndexRequest();
|
||||
waitsFor(function () {
|
||||
return provider.pendingRequests === 0;
|
||||
});
|
||||
|
||||
resolveAsyncTasks();
|
||||
resolveAsyncTasks();
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
||||
request: 'index',
|
||||
model: mockDomainObjects.a.getModel(),
|
||||
id: 'a'
|
||||
runs(function () {
|
||||
expect(provider.index)
|
||||
.toHaveBeenCalledWith('abc', models.abc);
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
@ -19,114 +19,205 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* 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.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
define([
|
||||
|
||||
describe("The generic search worker ", 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;
|
||||
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 () {
|
||||
var flag = false,
|
||||
workerOutput,
|
||||
resultsLength = 0;
|
||||
|
||||
// Search something that should return all objects
|
||||
runs(function () {
|
||||
worker.postMessage(
|
||||
{
|
||||
request: "search",
|
||||
input: "object",
|
||||
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;
|
||||
};
|
||||
|
||||
waitsFor(function () {
|
||||
return flag;
|
||||
}, "The worker should be searching", 1000);
|
||||
|
||||
runs(function () {
|
||||
expect(workerOutput).toBeDefined();
|
||||
expect(resultsLength).toEqual(numObjects);
|
||||
], function (
|
||||
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
describe('GenericSearchWorker', function () {
|
||||
// If this test fails, make sure this path is correct
|
||||
var worker,
|
||||
objectX,
|
||||
objectY,
|
||||
objectZ,
|
||||
itemsToIndex,
|
||||
onMessage,
|
||||
data,
|
||||
waitForResult;
|
||||
|
||||
beforeEach(function () {
|
||||
worker = new Worker(
|
||||
require.toUrl('platform/search/src/services/GenericSearchWorker.js')
|
||||
);
|
||||
|
||||
objectX = {
|
||||
id: 'x',
|
||||
model: {name: 'object xx'}
|
||||
};
|
||||
objectY = {
|
||||
id: 'y',
|
||||
model: {name: 'object yy'}
|
||||
};
|
||||
objectZ = {
|
||||
id: 'z',
|
||||
model: {name: 'object zz'}
|
||||
};
|
||||
itemsToIndex = [
|
||||
objectX,
|
||||
objectY,
|
||||
objectZ
|
||||
];
|
||||
|
||||
itemsToIndex.forEach(function (item) {
|
||||
worker.postMessage({
|
||||
request: 'index',
|
||||
id: item.id,
|
||||
model: item.model
|
||||
});
|
||||
});
|
||||
|
||||
it("searches return only matches", function () {
|
||||
var flag = false,
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
onMessage = jasmine.createSpy('onMessage');
|
||||
worker.addEventListener('message', onMessage);
|
||||
|
||||
waitForResult = function () {
|
||||
waitsFor(function () {
|
||||
return flag;
|
||||
}, "The worker should be searching", 1000);
|
||||
|
||||
runs(function () {
|
||||
expect(workerOutput).toBeDefined();
|
||||
expect(resultsLength).toEqual(1);
|
||||
expect(workerOutput.results[2]).toBeDefined();
|
||||
if (onMessage.calls.length > 0) {
|
||||
data = onMessage.calls[0].args[0].data;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
define(
|
||||
["../../src/services/SearchAggregator"],
|
||||
function (SearchAggregator) {
|
||||
"use strict";
|
||||
define([
|
||||
"../../src/services/SearchAggregator"
|
||||
], function (SearchAggregator) {
|
||||
"use strict";
|
||||
|
||||
describe("The search aggregator ", function () {
|
||||
var mockQ,
|
||||
mockPromise,
|
||||
mockProviders = [],
|
||||
aggregator,
|
||||
mockProviderResults = [],
|
||||
mockAggregatorResults,
|
||||
i;
|
||||
describe("SearchAggregator", function () {
|
||||
var $q,
|
||||
objectService,
|
||||
providers,
|
||||
aggregator;
|
||||
|
||||
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: {
|
||||
'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
|
||||
|
Loading…
x
Reference in New Issue
Block a user