Compare commits
182 Commits
prototype-
...
open229
Author | SHA1 | Date | |
---|---|---|---|
e59bedb969 | |||
f7f6b8d612 | |||
e4a14b7603 | |||
5706fa4567 | |||
796d6b800a | |||
b12bb55495 | |||
f42498ab60 | |||
ec56fe7bfd | |||
4abb48abd8 | |||
7f571415dc | |||
4f9a65a5fe | |||
6db7f056dc | |||
5a1d774b47 | |||
101e3bb346 | |||
a38d4829eb | |||
d5f1d45759 | |||
5ed34c1c30 | |||
5af3d575a2 | |||
b6d08726fb | |||
df3b0bd6fb | |||
5b475c9f64 | |||
a48370abd3 | |||
54d608adb2 | |||
5fba6f5ead | |||
b177c38656 | |||
59cd346911 | |||
e7e66bff4b | |||
ca62cc9066 | |||
01b6fda1f2 | |||
ae0cb63a92 | |||
5604bf6d69 | |||
c5fcc5a558 | |||
ea9f607bba | |||
0404303042 | |||
5677548298 | |||
bcc42d705e | |||
ab008ae497 | |||
3d59f6df0b | |||
4708ac0ec1 | |||
bd984abc0f | |||
b4a44dee8f | |||
942f617bd8 | |||
757cb0f015 | |||
90389ea910 | |||
0541f6edfa | |||
0ad22f6842 | |||
00c0019122 | |||
e80d094174 | |||
60ba80d307 | |||
c89aa026b0 | |||
8a3f77d784 | |||
15a5c593fa | |||
9e8152719f | |||
55745d281f | |||
8ba112498b | |||
44fc9423df | |||
bd8bbc6e8f | |||
2ee53b17db | |||
60e97eb94a | |||
ce5a650d8c | |||
5ff2e6b652 | |||
f30a2dd791 | |||
920c83771d | |||
a2d06583ca | |||
cc8a7e513f | |||
9dce3a04cc | |||
18cdaf1b53 | |||
e5d4376f06 | |||
6784c6567b | |||
71618ce4b9 | |||
e5ebbdaf7f | |||
6b805183b0 | |||
9723c65016 | |||
5d5425db04 | |||
7bd0e279b0 | |||
caee1f520f | |||
a8856c0612 | |||
b375ede217 | |||
0bd1d53d25 | |||
3fe386fcd6 | |||
74f289cb34 | |||
82b321b3f9 | |||
3bdaae292e | |||
b627de45ba | |||
2056f3aed0 | |||
b928b7d3f2 | |||
df6cd8f403 | |||
c6b7121cf0 | |||
86c61f7543 | |||
741fc57442 | |||
ed9a5b0890 | |||
aa23d358cc | |||
0f34d38451 | |||
f8ee244475 | |||
322d1c8389 | |||
c45bf45475 | |||
4ec243c6fb | |||
407d9881ff | |||
6ee622b3f5 | |||
099d70b8d9 | |||
3d996ac466 | |||
90828ef63d | |||
29bdc9d574 | |||
bf24ac7c93 | |||
59f094763b | |||
111b3bac09 | |||
67cf8d8cee | |||
b4dd95490c | |||
16e4c32709 | |||
a154c9c870 | |||
1e71df5ce9 | |||
5e9f38dadd | |||
552435b009 | |||
4ff03b081d | |||
e8d7093eb5 | |||
9bc4327c59 | |||
c0fda5b572 | |||
950578f09b | |||
117470068a | |||
0d47b7c47d | |||
c882b2d4c3 | |||
794231143e | |||
6b42d3bf4b | |||
1c5101eca6 | |||
3080861764 | |||
dbebf08500 | |||
847c356063 | |||
06bcd28558 | |||
f88e8ebb51 | |||
6d2b2fd81e | |||
608800ae63 | |||
07818b0a6d | |||
496cf85b7e | |||
833f57e284 | |||
cc97408433 | |||
93f8e61c40 | |||
9a63e99710 | |||
21739fffd9 | |||
77d81f899b | |||
fe3263fdfe | |||
ce42429fbd | |||
76151d09a0 | |||
ec7e6cc5b4 | |||
1ddce48f7e | |||
98b5ff3c77 | |||
14094a48fc | |||
8e2a2eeba5 | |||
0f63e4dde9 | |||
12efb47be7 | |||
a2fce8e56c | |||
78e5c0143b | |||
099591ad2e | |||
b5505f372f | |||
9ad860babd | |||
87e317a6f5 | |||
9d17768327 | |||
688718cad0 | |||
efb7611f6e | |||
d3ff0a258e | |||
4f18663c71 | |||
8c2a29e895 | |||
5b617295e9 | |||
53a3a2f459 | |||
64fae21d16 | |||
87f48aac35 | |||
e43a788a6d | |||
fa487e026e | |||
3701fd75dd | |||
d787e84fd4 | |||
1922e1e241 | |||
e52f53b7ff | |||
d1be256691 | |||
bf41d82a78 | |||
a4944717a1 | |||
70bbd3cf97 | |||
e3afaf0842 | |||
60f2f9fb6c | |||
37dede568c | |||
b3fb06ba3f | |||
b7a612127d | |||
2f4cf44229 | |||
73e959f95a |
2
Procfile
@ -1 +1 @@
|
||||
web: node app.js --port $PORT --include example/localstorage
|
||||
web: node app.js --port $PORT
|
||||
|
@ -6,6 +6,7 @@
|
||||
"platform/commonUI/browse",
|
||||
"platform/commonUI/edit",
|
||||
"platform/commonUI/dialog",
|
||||
"platform/commonUI/formats",
|
||||
"platform/commonUI/general",
|
||||
"platform/commonUI/inspect",
|
||||
"platform/commonUI/mobile",
|
||||
|
@ -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;
|
||||
}
|
||||
@ -179,13 +184,17 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
|
||||
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
|
||||
files.forEach(function (file) {
|
||||
var destination = file.replace(options['in'], options.out),
|
||||
destPath = path.dirname(destination);
|
||||
|
||||
destPath = path.dirname(destination),
|
||||
streamOptions = {};
|
||||
if (file.match(/png$/)){
|
||||
streamOptions.encoding = null;
|
||||
} else {
|
||||
streamOptions.encoding = 'utf8';
|
||||
}
|
||||
|
||||
mkdirp(destPath, function (err) {
|
||||
fs.createReadStream(file, { encoding: 'utf8' })
|
||||
.pipe(fs.createWriteStream(destination, {
|
||||
encoding: 'utf8'
|
||||
}));
|
||||
fs.createReadStream(file, streamOptions)
|
||||
.pipe(fs.createWriteStream(destination, streamOptions));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -35,16 +35,26 @@ in __any of these tiers__.
|
||||
* _DOM_: The rendered HTML document, composed from HTML templates which
|
||||
have been processed by AngularJS and will be updated by AngularJS
|
||||
to reflect changes from the presentation layer. User interactions
|
||||
are initiated from here and invoke behavior in the presentation layer.
|
||||
are initiated from here and invoke behavior in the presentation layer. HTML
|
||||
templates are written in Angular’s template syntax; see the [Angular documentation on templates](https://docs.angularjs.org/guide/templates).
|
||||
These describe the page as actually seen by the user. Conceptually,
|
||||
stylesheets (controlling the lookandfeel of the rendered templates) belong
|
||||
in this grouping as well.
|
||||
* [_Presentation layer_](#presentation-layer): The presentation layer
|
||||
is responsible for updating (and providing information to update)
|
||||
the displayed state of the application. The presentation layer consists
|
||||
primarily of _controllers_ and _directives_. The presentation layer is
|
||||
concerned with inspecting the information model and preparing it for
|
||||
display.
|
||||
* [_Information model_](#information-model): The information model
|
||||
describes the state and behavior of the objects with which the user
|
||||
interacts.
|
||||
* [_Information model_](#information-model): Provides a common (within Open MCT
|
||||
Web) set of interfaces for dealing with “things” domain objects within the
|
||||
system. Userfacing concerns in a Open MCT Web application are expressed as
|
||||
domain objects; examples include folders (used to organize other domain
|
||||
objects), layouts (used to build displays), or telemetry points (used as
|
||||
handles for streams of remote measurements.) These domain objects expose a
|
||||
common set of interfaces to allow reusable user interfaces to be built in the
|
||||
presentation and template tiers; the specifics of these behaviors are then
|
||||
mapped to interactions with underlying services.
|
||||
* [_Service infrastructure_](#service-infrastructure): The service
|
||||
infrastructure is responsible for providing the underlying general
|
||||
functionality needed to support the information model. This includes
|
||||
@ -52,7 +62,9 @@ in __any of these tiers__.
|
||||
back-end.
|
||||
* _Back-end_: The back-end is out of the scope of Open MCT Web, except
|
||||
for the interfaces which are utilized by adapters participating in the
|
||||
service infrastructure.
|
||||
service infrastructure. Includes the underlying persistence stores, telemetry
|
||||
streams, and so forth which the Open MCT Web client is being used to interact
|
||||
with.
|
||||
|
||||
## Application Start-up
|
||||
|
||||
|
@ -29,8 +29,9 @@
|
||||
Sections:
|
||||
<ul>
|
||||
<li><a href="api/">API</a></li>
|
||||
<li><a href="guide/">Developer Guide</a></li>
|
||||
<li><a href="architecture/">Architecture Overview</a></li>
|
||||
<li><a href="guide/">Developer Guide</a></li>
|
||||
<li><a href="tutorials/">Tutorials</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
BIN
docs/src/tutorials/images/add-task.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/src/tutorials/images/bar-plot-2.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
docs/src/tutorials/images/bar-plot-3.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/src/tutorials/images/bar-plot-4.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
docs/src/tutorials/images/bar-plot.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs/src/tutorials/images/chrome.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
docs/src/tutorials/images/remove-task.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/src/tutorials/images/telemetry-1.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/src/tutorials/images/telemetry-2.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
docs/src/tutorials/images/telemetry-3.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
docs/src/tutorials/images/todo-edit.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
docs/src/tutorials/images/todo-list.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
docs/src/tutorials/images/todo-restyled.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
docs/src/tutorials/images/todo-selection.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/src/tutorials/images/todo.png
Normal file
After Width: | Height: | Size: 43 KiB |
3055
docs/src/tutorials/index.md
Normal file
@ -16,6 +16,23 @@
|
||||
"implementation": "SinewaveLimitCapability.js"
|
||||
}
|
||||
],
|
||||
"formats": [
|
||||
{
|
||||
"key": "example.delta",
|
||||
"implementation": "SinewaveDeltaFormat.js"
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "TIME_CONDUCTOR_DOMAINS",
|
||||
"value": [
|
||||
{ "key": "time", "name": "Time" },
|
||||
{ "key": "yesterday", "name": "Yesterday" },
|
||||
{ "key": "delta", "name": "Delta", "format": "example.delta" }
|
||||
],
|
||||
"priority": -1
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"key": "generator",
|
||||
@ -38,6 +55,11 @@
|
||||
{
|
||||
"key": "yesterday",
|
||||
"name": "Yesterday"
|
||||
},
|
||||
{
|
||||
"key": "delta",
|
||||
"name": "Delta",
|
||||
"format": "example.delta"
|
||||
}
|
||||
],
|
||||
"ranges": [
|
||||
|
26
example/generator/src/SinewaveConstants.js
Normal file
@ -0,0 +1,26 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise*/
|
||||
|
||||
define({
|
||||
START_TIME: Date.now() - 24 * 60 * 60 * 1000 // Now minus a day.
|
||||
});
|
68
example/generator/src/SinewaveDeltaFormat.js
Normal file
@ -0,0 +1,68 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise*/
|
||||
|
||||
define(
|
||||
['./SinewaveConstants', 'moment'],
|
||||
function (SinewaveConstants, moment) {
|
||||
"use strict";
|
||||
|
||||
var START_TIME = SinewaveConstants.START_TIME,
|
||||
FORMAT_REGEX = /^-?\d+:\d+:\d+$/,
|
||||
SECOND = 1000,
|
||||
MINUTE = SECOND * 60,
|
||||
HOUR = MINUTE * 60;
|
||||
|
||||
function SinewaveDeltaFormat() {
|
||||
}
|
||||
|
||||
function twoDigit(v) {
|
||||
return v >= 10 ? String(v) : ('0' + v);
|
||||
}
|
||||
|
||||
SinewaveDeltaFormat.prototype.format = function (value) {
|
||||
var delta = Math.abs(value - START_TIME),
|
||||
negative = value < START_TIME,
|
||||
seconds = Math.floor(delta / SECOND) % 60,
|
||||
minutes = Math.floor(delta / MINUTE) % 60,
|
||||
hours = Math.floor(delta / HOUR);
|
||||
return (negative ? "-" : "") +
|
||||
[ hours, minutes, seconds ].map(twoDigit).join(":");
|
||||
};
|
||||
|
||||
SinewaveDeltaFormat.prototype.validate = function (text) {
|
||||
return FORMAT_REGEX.test(text);
|
||||
};
|
||||
|
||||
SinewaveDeltaFormat.prototype.parse = function (text) {
|
||||
var negative = text[0] === "-",
|
||||
parts = text.replace("-", "").split(":");
|
||||
return [ HOUR, MINUTE, SECOND ].map(function (sz, i) {
|
||||
return parseInt(parts[i], 10) * sz;
|
||||
}).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}, 0) * (negative ? -1 : 1) + START_TIME;
|
||||
};
|
||||
|
||||
return SinewaveDeltaFormat;
|
||||
}
|
||||
);
|
@ -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"
|
||||
|
@ -25,12 +25,12 @@
|
||||
* Module defining SinewaveTelemetry. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
['./SinewaveConstants'],
|
||||
function (SinewaveConstants) {
|
||||
"use strict";
|
||||
|
||||
var ONE_DAY = 60 * 60 * 24,
|
||||
firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY;
|
||||
firstObservedTime = Math.floor(SinewaveConstants.START_TIME / 1000);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -58,6 +58,9 @@ define(
|
||||
};
|
||||
|
||||
generatorData.getDomainValue = function (i, domain) {
|
||||
// delta uses the same numeric values as the default domain,
|
||||
// so it's not checked for here, just formatted for display
|
||||
// differently.
|
||||
return (i + offset) * 1000 + firstTime * 1000 -
|
||||
(domain === 'yesterday' ? ONE_DAY : 0);
|
||||
};
|
||||
|
@ -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";
|
||||
|
||||
|
26
platform/commonUI/formats/bundle.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Time services bundle",
|
||||
"description": "Defines interfaces and provides default implementations for handling different time systems.",
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"provides": "formatService",
|
||||
"type": "provider",
|
||||
"implementation": "FormatProvider.js",
|
||||
"depends": [ "formats[]", "$log" ]
|
||||
}
|
||||
],
|
||||
"formats": [
|
||||
{
|
||||
"key": "utc",
|
||||
"implementation": "UTCTimeFormat.js"
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "DEFAULT_TIME_FORMAT",
|
||||
"value": "utc"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
113
platform/commonUI/formats/src/FormatProvider.js
Normal file
@ -0,0 +1,113 @@
|
||||
/*****************************************************************************
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* An object used to convert between numeric values and text values,
|
||||
* typically used to display these values to the user and to convert
|
||||
* user input to a numeric format, particularly for time formats.
|
||||
* @interface {Format}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse text (typically user input) to a numeric value.
|
||||
* Behavior is undefined when the text cannot be parsed;
|
||||
* `validate` should be called first if the text may be invalid.
|
||||
* @method parse
|
||||
* @memberof Format#
|
||||
* @param {string} text the text to parse
|
||||
* @returns {number} the parsed numeric value
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine whether or not some text (typically user input) can
|
||||
* be parsed to a numeric value by this format.
|
||||
* @method validate
|
||||
* @memberof Format#
|
||||
* @param {string} text the text to parse
|
||||
* @returns {boolean} true if the text can be parsed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a numeric value to a text value for display using
|
||||
* this format.
|
||||
* @method format
|
||||
* @memberof Format#
|
||||
* @param {number} value the numeric value to format
|
||||
* @returns {string} the text representation of the value
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides access to `Format` objects which can be used to
|
||||
* convert values between human-readable text and numeric
|
||||
* representations.
|
||||
* @interface FormatService
|
||||
*/
|
||||
|
||||
/**
|
||||
* Look up a format by its symbolic identifier.
|
||||
* @param {string} key the identifier for this format
|
||||
* @returns {Format} the format, or `undefined` if no such format
|
||||
* is known.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides formats from the `formats` extension category.
|
||||
* @constructor
|
||||
* @implements {FormatService}
|
||||
* @memberof platform/commonUI/formats
|
||||
* @param {Array.<function(new : Format)>} format constructors,
|
||||
* from the `formats` extension category.
|
||||
*/
|
||||
function FormatProvider(formats, $log) {
|
||||
var formatMap = {};
|
||||
|
||||
function addToMap(Format) {
|
||||
var key = Format.key;
|
||||
if (key && !formatMap[key]) {
|
||||
formatMap[key] = new Format();
|
||||
}
|
||||
}
|
||||
|
||||
formats.forEach(addToMap);
|
||||
this.formatMap = formatMap;
|
||||
this.$log = $log;
|
||||
}
|
||||
|
||||
FormatProvider.prototype.getFormat = function (key) {
|
||||
var format = this.formatMap[key];
|
||||
if (!format) {
|
||||
this.$log.warn("No format found for " + key);
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
return FormatProvider;
|
||||
|
||||
});
|
63
platform/commonUI/formats/src/UTCTimeFormat.js
Normal file
@ -0,0 +1,63 @@
|
||||
/*****************************************************************************
|
||||
* 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([
|
||||
'moment'
|
||||
], function (
|
||||
moment
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
||||
DATE_FORMATS = [
|
||||
DATE_FORMAT,
|
||||
"YYYY-MM-DD HH:mm",
|
||||
"YYYY-MM-DD"
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Formatter for UTC timestamps. Interprets numeric values as
|
||||
* milliseconds since the start of 1970.
|
||||
*
|
||||
* @implements {Format}
|
||||
* @constructor
|
||||
* @memberof platform/commonUI/formats
|
||||
*/
|
||||
function UTCTimeFormat() {
|
||||
}
|
||||
|
||||
UTCTimeFormat.prototype.format = function (value) {
|
||||
return moment.utc(value).format(DATE_FORMAT);
|
||||
};
|
||||
|
||||
UTCTimeFormat.prototype.parse = function (text) {
|
||||
return moment.utc(text, DATE_FORMATS).valueOf();
|
||||
};
|
||||
|
||||
UTCTimeFormat.prototype.validate = function (text) {
|
||||
return moment.utc(text, DATE_FORMATS).isValid();
|
||||
};
|
||||
|
||||
return UTCTimeFormat;
|
||||
});
|
70
platform/commonUI/formats/test/FormatProviderSpec.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
['../src/FormatProvider'],
|
||||
function (FormatProvider) {
|
||||
'use strict';
|
||||
|
||||
var KEYS = [ 'a', 'b', 'c' ];
|
||||
|
||||
describe("The FormatProvider", function () {
|
||||
var mockFormats,
|
||||
mockLog,
|
||||
mockFormatInstances,
|
||||
provider;
|
||||
|
||||
beforeEach(function () {
|
||||
mockFormatInstances = KEYS.map(function (k) {
|
||||
return jasmine.createSpyObj(
|
||||
'format-' + k,
|
||||
[ 'parse', 'validate', 'format' ]
|
||||
);
|
||||
});
|
||||
mockLog = jasmine.createSpyObj('$log', ['error', 'warn']);
|
||||
// Return constructors
|
||||
mockFormats = KEYS.map(function (k, i) {
|
||||
function MockFormat() { return mockFormatInstances[i]; }
|
||||
MockFormat.key = k;
|
||||
return MockFormat;
|
||||
});
|
||||
provider = new FormatProvider(mockFormats, mockLog);
|
||||
});
|
||||
|
||||
it("looks up formats by key", function () {
|
||||
KEYS.forEach(function (k, i) {
|
||||
expect(provider.getFormat(k))
|
||||
.toEqual(mockFormatInstances[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it("warns about unknown formats", function () {
|
||||
provider.getFormat('a'); // known format
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
provider.getFormat('some-unknown-format');
|
||||
expect(mockLog.warn).toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
56
platform/commonUI/formats/test/UTCTimeFormatSpec.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
['../src/UTCTimeFormat', 'moment'],
|
||||
function (UTCTimeFormat, moment) {
|
||||
'use strict';
|
||||
|
||||
describe("The UTCTimeFormat", function () {
|
||||
var format;
|
||||
|
||||
beforeEach(function () {
|
||||
format = new UTCTimeFormat();
|
||||
});
|
||||
|
||||
it("formats UTC timestamps", function () {
|
||||
var timestamp = 12345670000,
|
||||
formatted = format.format(timestamp);
|
||||
expect(formatted).toEqual(jasmine.any(String));
|
||||
expect(moment.utc(formatted).valueOf()).toEqual(timestamp);
|
||||
});
|
||||
|
||||
it("validates time inputs", function () {
|
||||
expect(format.validate("1977-05-25 11:21:22")).toBe(true);
|
||||
expect(format.validate("garbage text")).toBe(false);
|
||||
});
|
||||
|
||||
it("parses valid input", function () {
|
||||
var text = "1977-05-25 11:21:22",
|
||||
parsed = format.parse(text);
|
||||
expect(parsed).toEqual(jasmine.any(Number));
|
||||
expect(parsed).toEqual(moment.utc(text).valueOf());
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
4
platform/commonUI/formats/test/suite.json
Normal file
@ -0,0 +1,4 @@
|
||||
[
|
||||
"FormatProvider",
|
||||
"UTCTimeFormat"
|
||||
]
|
@ -53,13 +53,18 @@
|
||||
{
|
||||
"key": "TimeRangeController",
|
||||
"implementation": "controllers/TimeRangeController.js",
|
||||
"depends": [ "$scope", "now" ]
|
||||
"depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT", "now" ]
|
||||
},
|
||||
{
|
||||
"key": "DateTimePickerController",
|
||||
"implementation": "controllers/DateTimePickerController.js",
|
||||
"depends": [ "$scope", "now" ]
|
||||
},
|
||||
{
|
||||
"key": "DateTimeFieldController",
|
||||
"implementation": "controllers/DateTimeFieldController.js",
|
||||
"depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT" ]
|
||||
},
|
||||
{
|
||||
"key": "TreeNodeController",
|
||||
"implementation": "controllers/TreeNodeController.js",
|
||||
@ -242,6 +247,10 @@
|
||||
{
|
||||
"key": "datetime-picker",
|
||||
"templateUrl": "templates/controls/datetime-picker.html"
|
||||
},
|
||||
{
|
||||
"key": "datetime-field",
|
||||
"templateUrl": "templates/controls/datetime-field.html"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
|
@ -106,3 +106,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; }
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<span class="s-btn"
|
||||
ng-controller="DateTimeFieldController">
|
||||
<input type="text"
|
||||
ng-model="textValue"
|
||||
ng-class="{ error: textInvalid }">
|
||||
</input>
|
||||
<a class="ui-symbol icon icon-calendar"
|
||||
ng-if="structure.format === 'utc' || !structure.format"
|
||||
ng-click="pickerActive = !pickerActive">
|
||||
</a>
|
||||
<mct-popup ng-if="pickerActive">
|
||||
<div mct-click-elsewhere="pickerActive = false">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel"
|
||||
field="field"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
</span>
|
@ -19,84 +19,67 @@
|
||||
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">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format }"
|
||||
ng-model="ngModel.outer"
|
||||
field="'start'"
|
||||
class="time-range-start">
|
||||
</mct-control>
|
||||
</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">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format }"
|
||||
ng-model="ngModel.outer"
|
||||
field="'end'"
|
||||
class="time-range-end">
|
||||
</mct-control>
|
||||
</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 track by $index"
|
||||
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>
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
var UNRECOGNIZED_FORMAT_ERROR =
|
||||
"Unrecognized format for date-time field.";
|
||||
|
||||
/**
|
||||
* Controller to support the date-time entry field.
|
||||
*
|
||||
* Accepts a `format` property in the `structure` attribute
|
||||
* which allows a date/time to be specified via its symbolic
|
||||
* key (as will be used to look up said format from the
|
||||
* `formatService`.)
|
||||
*
|
||||
* {@see FormatService}
|
||||
* @constructor
|
||||
* @memberof platform/commonUI/general
|
||||
* @param $scope the Angular scope for this controller
|
||||
* @param {FormatService} formatService the service to user to format
|
||||
* domain values
|
||||
* @param {string} defaultFormat the format to request when no
|
||||
* format has been otherwise specified
|
||||
*/
|
||||
function DateTimeFieldController($scope, formatService, defaultFormat) {
|
||||
var formatter = formatService.getFormat(defaultFormat);
|
||||
|
||||
function updateFromModel(value) {
|
||||
// Only reformat if the value is different from user
|
||||
// input (to avoid reformatting valid input while typing.)
|
||||
if (!formatter.validate($scope.textValue) ||
|
||||
formatter.parse($scope.textValue) !== value) {
|
||||
$scope.textValue = formatter.format(value);
|
||||
$scope.textInvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFromView(textValue) {
|
||||
$scope.textInvalid = !formatter.validate(textValue);
|
||||
if (!$scope.textInvalid) {
|
||||
$scope.ngModel[$scope.field] =
|
||||
formatter.parse(textValue);
|
||||
}
|
||||
}
|
||||
|
||||
function setFormat(format) {
|
||||
formatter = formatService.getFormat(format || defaultFormat);
|
||||
if (!formatter) {
|
||||
throw new Error(UNRECOGNIZED_FORMAT_ERROR);
|
||||
}
|
||||
updateFromModel($scope.ngModel[$scope.field]);
|
||||
}
|
||||
|
||||
$scope.$watch('structure.format', setFormat);
|
||||
$scope.$watch('ngModel[field]', updateFromModel);
|
||||
$scope.$watch('textValue', updateFromView);
|
||||
}
|
||||
|
||||
return DateTimeFieldController;
|
||||
}
|
||||
);
|
@ -26,25 +26,34 @@ define(
|
||||
function (moment) {
|
||||
"use strict";
|
||||
|
||||
var
|
||||
DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
|
||||
TICK_SPACING_PX = 150;
|
||||
var TICK_SPACING_PX = 150,
|
||||
UNRECOGNIZED_FORMAT_ERROR =
|
||||
"Unrecognized format for time range control.";
|
||||
|
||||
|
||||
/**
|
||||
* Controller used by the `time-controller` template.
|
||||
* @memberof platform/commonUI/general
|
||||
* @constructor
|
||||
* @param $scope the Angular scope for this controller
|
||||
* @param {FormatService} formatService the service to user to format
|
||||
* domain values
|
||||
* @param {string} defaultFormat the format to request when no
|
||||
* format has been otherwise specified
|
||||
* @param {Function} now a function to return current system time
|
||||
*/
|
||||
function TimeConductorController($scope, now) {
|
||||
function TimeRangeController($scope, formatService, defaultFormat, now) {
|
||||
var tickCount = 2,
|
||||
innerMinimumSpan = 1000, // 1 second
|
||||
outerMinimumSpan = 1000 * 60 * 60, // 1 hour
|
||||
initialDragValue;
|
||||
initialDragValue,
|
||||
formatter = formatService.getFormat(defaultFormat);
|
||||
|
||||
function formatTimestamp(ts) {
|
||||
return moment.utc(ts).format(DATE_FORMAT);
|
||||
return formatter.format(ts);
|
||||
}
|
||||
|
||||
// From 0.0-1.0 to "0%"-"1%"
|
||||
// From 0.0-1.0 to "0%"-"100%"
|
||||
function toPercent(p) {
|
||||
return (100 * p) + "%";
|
||||
}
|
||||
@ -94,22 +103,14 @@ define(
|
||||
}
|
||||
|
||||
function updateViewFromModel(ngModel) {
|
||||
var t = now();
|
||||
|
||||
ngModel = ngModel || {};
|
||||
ngModel.outer = ngModel.outer || defaultBounds();
|
||||
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);
|
||||
|
||||
// Then various updates for the inner span
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
|
||||
// Stick it back is scope (in case we just set defaults)
|
||||
$scope.ngModel = ngModel;
|
||||
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
@ -129,7 +130,8 @@ define(
|
||||
}
|
||||
|
||||
function toMillis(pixels) {
|
||||
var span = $scope.ngModel.outer.end - $scope.ngModel.outer.start;
|
||||
var span =
|
||||
$scope.ngModel.outer.end - $scope.ngModel.outer.start;
|
||||
return (pixels / $scope.spanWidth) * span;
|
||||
}
|
||||
|
||||
@ -178,6 +180,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 +194,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 +215,19 @@ define(
|
||||
ngModel.inner.start
|
||||
);
|
||||
|
||||
$scope.endOuterText = formatTimestamp(t);
|
||||
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateFormat(key) {
|
||||
formatter = formatService.getFormat(key || defaultFormat);
|
||||
|
||||
if (!formatter) {
|
||||
throw new Error(UNRECOGNIZED_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
updateViewForInnerSpanFromModel($scope.ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
$scope.startLeftDrag = startLeftDrag;
|
||||
@ -222,7 +237,6 @@ define(
|
||||
$scope.rightDrag = rightDrag;
|
||||
$scope.middleDrag = middleDrag;
|
||||
|
||||
$scope.state = false;
|
||||
$scope.ticks = [];
|
||||
|
||||
// Initialize scope to defaults
|
||||
@ -232,8 +246,9 @@ define(
|
||||
$scope.$watch("spanWidth", updateSpanWidth);
|
||||
$scope.$watch("ngModel.outer.start", updateOuterStart);
|
||||
$scope.$watch("ngModel.outer.end", updateOuterEnd);
|
||||
$scope.$watch("parameters.format", updateFormat);
|
||||
}
|
||||
|
||||
return TimeConductorController;
|
||||
return TimeRangeController;
|
||||
}
|
||||
);
|
||||
|
@ -0,0 +1,183 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/controllers/DateTimeFieldController", "moment"],
|
||||
function (DateTimeFieldController, moment) {
|
||||
'use strict';
|
||||
|
||||
var TEST_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
describe("The DateTimeFieldController", function () {
|
||||
var mockScope,
|
||||
mockFormatService,
|
||||
mockFormat,
|
||||
controller;
|
||||
|
||||
function fireWatch(expr, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj('$scope', ['$watch']);
|
||||
mockFormatService =
|
||||
jasmine.createSpyObj('formatService', ['getFormat']);
|
||||
mockFormat = jasmine.createSpyObj('format', [
|
||||
'parse',
|
||||
'validate',
|
||||
'format'
|
||||
]);
|
||||
|
||||
mockFormatService.getFormat.andReturn(mockFormat);
|
||||
|
||||
mockFormat.validate.andCallFake(function (text) {
|
||||
return moment.utc(text, TEST_FORMAT).isValid();
|
||||
});
|
||||
mockFormat.parse.andCallFake(function (text) {
|
||||
return moment.utc(text, TEST_FORMAT).valueOf();
|
||||
});
|
||||
mockFormat.format.andCallFake(function (value) {
|
||||
return moment.utc(value).format(TEST_FORMAT);
|
||||
});
|
||||
|
||||
mockScope.ngModel = { testField: 12321 };
|
||||
mockScope.field = "testField";
|
||||
mockScope.structure = { format: "someFormat" };
|
||||
|
||||
controller = new DateTimeFieldController(
|
||||
mockScope,
|
||||
mockFormatService
|
||||
);
|
||||
});
|
||||
|
||||
it("updates models from user-entered text", function () {
|
||||
var newText = "1977-05-25 17:30:00";
|
||||
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
expect(mockScope.ngModel.testField)
|
||||
.toEqual(mockFormat.parse(newText));
|
||||
expect(mockScope.textInvalid).toBeFalsy();
|
||||
});
|
||||
|
||||
it("updates text from model values", function () {
|
||||
var testTime = mockFormat.parse("1977-05-25 17:30:00");
|
||||
mockScope.ngModel.testField = testTime;
|
||||
fireWatch("ngModel[field]", testTime);
|
||||
expect(mockScope.textValue).toEqual("1977-05-25 17:30:00");
|
||||
});
|
||||
|
||||
describe("when user input is invalid", function () {
|
||||
var newText, oldValue;
|
||||
|
||||
beforeEach(function () {
|
||||
newText = "Not a date";
|
||||
oldValue = mockScope.ngModel.testField;
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
});
|
||||
|
||||
it("displays error state", function () {
|
||||
expect(mockScope.textInvalid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not modify model state", function () {
|
||||
expect(mockScope.ngModel.testField).toEqual(oldValue);
|
||||
});
|
||||
|
||||
it("does not modify user input", function () {
|
||||
expect(mockScope.textValue).toEqual(newText);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not modify valid but irregular user input", function () {
|
||||
// Don't want the controller "fixing" bad or
|
||||
// irregularly-formatted input out from under
|
||||
// the user's fingertips.
|
||||
var newText = "2015-3-3 01:02:04",
|
||||
oldValue = mockScope.ngModel.testField;
|
||||
|
||||
mockFormat.validate.andReturn(true);
|
||||
mockFormat.parse.andReturn(42);
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
|
||||
expect(mockScope.textValue).toEqual(newText);
|
||||
expect(mockScope.ngModel.testField).toEqual(42);
|
||||
expect(mockScope.ngModel.testField).not.toEqual(oldValue);
|
||||
});
|
||||
|
||||
it("obtains a format from the format service", function () {
|
||||
fireWatch('structure.format', mockScope.structure.format);
|
||||
expect(mockFormatService.getFormat)
|
||||
.toHaveBeenCalledWith(mockScope.structure.format);
|
||||
});
|
||||
|
||||
it("throws an error for unknown formats", function () {
|
||||
mockFormatService.getFormat.andReturn(undefined);
|
||||
expect(function () {
|
||||
fireWatch("structure.format", "some-format");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe("using the obtained format", function () {
|
||||
var testValue = 1234321,
|
||||
testText = "some text";
|
||||
|
||||
beforeEach(function () {
|
||||
mockFormat.validate.andReturn(true);
|
||||
mockFormat.parse.andReturn(testValue);
|
||||
mockFormat.format.andReturn(testText);
|
||||
});
|
||||
|
||||
it("parses user input", function () {
|
||||
var newText = "some other new text";
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
expect(mockFormat.parse).toHaveBeenCalledWith(newText);
|
||||
expect(mockScope.ngModel.testField).toEqual(testValue);
|
||||
});
|
||||
|
||||
it("validates user input", function () {
|
||||
var newText = "some other new text";
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
expect(mockFormat.validate).toHaveBeenCalledWith(newText);
|
||||
});
|
||||
|
||||
it("formats model data for display", function () {
|
||||
var newValue = 42;
|
||||
mockScope.ngModel.testField = newValue;
|
||||
fireWatch("ngModel[field]", newValue);
|
||||
expect(mockFormat.format).toHaveBeenCalledWith(newValue);
|
||||
expect(mockScope.textValue).toEqual(testText);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -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,
|
||||
@ -33,7 +33,10 @@ define(
|
||||
|
||||
describe("The TimeRangeController", function () {
|
||||
var mockScope,
|
||||
mockFormatService,
|
||||
testDefaultFormat,
|
||||
mockNow,
|
||||
mockFormat,
|
||||
controller;
|
||||
|
||||
function fireWatch(expr, value) {
|
||||
@ -57,8 +60,30 @@ define(
|
||||
"$scope",
|
||||
[ "$apply", "$watch", "$watchCollection" ]
|
||||
);
|
||||
mockFormatService = jasmine.createSpyObj(
|
||||
"formatService",
|
||||
[ "getFormat" ]
|
||||
);
|
||||
testDefaultFormat = 'utc';
|
||||
mockFormat = jasmine.createSpyObj(
|
||||
"format",
|
||||
[ "validate", "format", "parse" ]
|
||||
);
|
||||
|
||||
mockFormatService.getFormat.andReturn(mockFormat);
|
||||
|
||||
mockFormat.format.andCallFake(function (value) {
|
||||
return moment.utc(value).format("YYYY-MM-DD HH:mm:ss");
|
||||
});
|
||||
|
||||
mockNow = jasmine.createSpy('now');
|
||||
controller = new TimeRangeController(mockScope, mockNow);
|
||||
|
||||
controller = new TimeRangeController(
|
||||
mockScope,
|
||||
mockFormatService,
|
||||
testDefaultFormat,
|
||||
mockNow
|
||||
);
|
||||
});
|
||||
|
||||
it("watches the model that was passed in", function () {
|
||||
@ -166,6 +191,22 @@ define(
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toBeGreaterThan(mockScope.ngModel.inner.start);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("watches for changes in format selection", function () {
|
||||
expect(mockFormatService.getFormat)
|
||||
.not.toHaveBeenCalledWith('test-format');
|
||||
fireWatch("parameters.format", 'test-format');
|
||||
expect(mockFormatService.getFormat)
|
||||
.toHaveBeenCalledWith('test-format');
|
||||
});
|
||||
|
||||
it("throws an error for unknown formats", function () {
|
||||
mockFormatService.getFormat.andReturn(undefined);
|
||||
expect(function () {
|
||||
fireWatch("parameters.format", "some-format");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -3,6 +3,7 @@
|
||||
"controllers/BottomBarController",
|
||||
"controllers/ClickAwayController",
|
||||
"controllers/ContextMenuController",
|
||||
"controllers/DateTimeFieldController",
|
||||
"controllers/DateTimePickerController",
|
||||
"controllers/GetterSetterController",
|
||||
"controllers/SelectorController",
|
||||
|
@ -55,11 +55,6 @@ define(
|
||||
self.trackPosition(event);
|
||||
};
|
||||
|
||||
// Also make sure we dismiss bubble if representation is destroyed
|
||||
// before the mouse actually leaves it
|
||||
this.scopeOff =
|
||||
element.scope().$on('$destroy', this.hideBubbleCallback);
|
||||
|
||||
this.element = element;
|
||||
this.$timeout = $timeout;
|
||||
this.infoService = infoService;
|
||||
@ -143,7 +138,6 @@ define(
|
||||
this.hideBubble();
|
||||
// ...and detach listeners
|
||||
this.element.off('mouseenter', this.showBubbleCallback);
|
||||
this.scopeOff();
|
||||
};
|
||||
|
||||
return InfoGesture;
|
||||
|
@ -20,7 +20,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* line 5, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 5, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
@ -41,38 +41,38 @@ time, mark, audio, video {
|
||||
font-size: 100%;
|
||||
vertical-align: baseline; }
|
||||
|
||||
/* line 22, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 22, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html {
|
||||
line-height: 1; }
|
||||
|
||||
/* line 24, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 24, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
ol, ul {
|
||||
list-style: none; }
|
||||
|
||||
/* line 26, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 26, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0; }
|
||||
|
||||
/* line 28, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 28, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
caption, th, td {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
vertical-align: middle; }
|
||||
|
||||
/* line 30, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 30, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q, blockquote {
|
||||
quotes: none; }
|
||||
/* line 103, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 103, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q:before, q:after, blockquote:before, blockquote:after {
|
||||
content: "";
|
||||
content: none; }
|
||||
|
||||
/* line 32, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 32, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
a img {
|
||||
border: none; }
|
||||
|
||||
/* line 116, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 116, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
|
||||
display: block; }
|
||||
|
||||
@ -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
|
||||
@ -657,46 +658,70 @@ mct-container {
|
||||
color: #ff3c00;
|
||||
content: "!"; }
|
||||
|
||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||
[class*="s-limit"]:before {
|
||||
display: inline-block;
|
||||
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: "ë"; }
|
||||
|
||||
/* 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: "í"; }
|
||||
|
||||
/*[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;
|
||||
}
|
||||
}*/
|
||||
/* 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: "ì"; }
|
||||
.s-limit-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
|
||||
/* 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: "î"; }
|
||||
.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.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 36, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||
content: "ë"; }
|
||||
/* line 37, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||
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 48, ../../../../general/res/sass/_limits.scss */
|
||||
:not(tr)[class*="s-limit"].s-limit-upr:before {
|
||||
content: "ë"; }
|
||||
/* line 49, ../../../../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 {
|
||||
@ -4142,32 +4167,10 @@ span.req {
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
position: relative; }
|
||||
/* line 228, ../../../../general/res/sass/search/_search.scss */
|
||||
/* line 226, ../../../../general/res/sass/search/_search.scss */
|
||||
.search .search-scroll .load-icon {
|
||||
position: relative;
|
||||
/* &.loading {
|
||||
pointer-events: none;
|
||||
margin-left: $leftMargin;
|
||||
|
||||
.title-label {
|
||||
// Text styling
|
||||
font-style: italic;
|
||||
font-size: .9em;
|
||||
opacity: 0.5;
|
||||
|
||||
// Text positioning
|
||||
margin-left: $iconWidth + $leftMargin;
|
||||
line-height: 24px;
|
||||
}
|
||||
.wait-spinner {
|
||||
margin-left: $leftMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.loading) {
|
||||
cursor: pointer;
|
||||
}*/ }
|
||||
/* line 255, ../../../../general/res/sass/search/_search.scss */
|
||||
position: relative; }
|
||||
/* line 230, ../../../../general/res/sass/search/_search.scss */
|
||||
.search .search-scroll .load-more-button {
|
||||
margin-top: 5px 0;
|
||||
font-size: 0.8em;
|
||||
@ -4438,21 +4441,7 @@ ul.tree {
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 3px;
|
||||
position: relative;
|
||||
/*
|
||||
&.loading {
|
||||
pointer-events: none;
|
||||
.label {
|
||||
opacity: 0.5;
|
||||
.title-label {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
.wait-spinner {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
*/ }
|
||||
position: relative; }
|
||||
/* line 48, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .view-control,
|
||||
.search-result-item .view-control {
|
||||
@ -4499,7 +4488,7 @@ ul.tree {
|
||||
.search-result-item .label .type-icon .icon.l-icon-alert {
|
||||
position: absolute;
|
||||
z-index: 2; }
|
||||
/* line 90, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 89, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .type-icon .icon.l-icon-alert,
|
||||
.search-result-item .label .type-icon .icon.l-icon-alert {
|
||||
color: #ff3c00;
|
||||
@ -4509,7 +4498,7 @@ ul.tree {
|
||||
width: 8px;
|
||||
top: 1px;
|
||||
right: -2px; }
|
||||
/* line 96, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 95, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .type-icon .icon.l-icon-link,
|
||||
.search-result-item .label .type-icon .icon.l-icon-link {
|
||||
color: #49dedb;
|
||||
@ -4519,7 +4508,7 @@ ul.tree {
|
||||
width: 8px;
|
||||
left: -3px;
|
||||
bottom: 0px; }
|
||||
/* line 104, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 103, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .title-label,
|
||||
.search-result-item .label .title-label {
|
||||
overflow: hidden;
|
||||
@ -4535,47 +4524,47 @@ ul.tree {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
/* line 130, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 113, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected,
|
||||
.search-result-item.selected {
|
||||
background: #006080;
|
||||
color: #cccccc; }
|
||||
/* line 133, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 116, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected .view-control,
|
||||
.search-result-item.selected .view-control {
|
||||
color: rgba(255, 255, 255, 0.3); }
|
||||
/* line 136, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 119, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected .label .type-icon,
|
||||
.search-result-item.selected .label .type-icon {
|
||||
color: #cccccc; }
|
||||
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
||||
/* line 144, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 127, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.selected):hover,
|
||||
.search-result-item:not(.selected):hover {
|
||||
background: rgba(153, 153, 153, 0.1);
|
||||
color: #cccccc; }
|
||||
/* line 150, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 130, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.selected):hover .icon,
|
||||
.search-result-item:not(.selected):hover .icon {
|
||||
color: #33ccff; } }
|
||||
/* line 157, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 137, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.loading),
|
||||
.search-result-item:not(.loading) {
|
||||
cursor: pointer; }
|
||||
/* line 161, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 141, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .context-trigger,
|
||||
.search-result-item .context-trigger {
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
right: 3px; }
|
||||
/* line 167, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 146, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .context-trigger .invoke-menu,
|
||||
.search-result-item .context-trigger .invoke-menu {
|
||||
font-size: 0.75em;
|
||||
height: 0.9rem;
|
||||
line-height: 0.9rem; }
|
||||
|
||||
/* line 176, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 155, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label {
|
||||
left: 15px; }
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* line 5, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 5, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
@ -41,38 +41,38 @@ time, mark, audio, video {
|
||||
font-size: 100%;
|
||||
vertical-align: baseline; }
|
||||
|
||||
/* line 22, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 22, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html {
|
||||
line-height: 1; }
|
||||
|
||||
/* line 24, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 24, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
ol, ul {
|
||||
list-style: none; }
|
||||
|
||||
/* line 26, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 26, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0; }
|
||||
|
||||
/* line 28, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 28, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
caption, th, td {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
vertical-align: middle; }
|
||||
|
||||
/* line 30, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 30, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q, blockquote {
|
||||
quotes: none; }
|
||||
/* line 103, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 103, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q:before, q:after, blockquote:before, blockquote:after {
|
||||
content: "";
|
||||
content: none; }
|
||||
|
||||
/* line 32, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 32, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
a img {
|
||||
border: none; }
|
||||
|
||||
/* line 116, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 116, ../../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
|
||||
display: block; }
|
||||
|
||||
@ -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
|
||||
@ -657,46 +658,70 @@ mct-container {
|
||||
color: #ff3c00;
|
||||
content: "!"; }
|
||||
|
||||
/* line 13, ../../../../general/res/sass/_limits.scss */
|
||||
[class*="s-limit"]:before {
|
||||
display: inline-block;
|
||||
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: "ë"; }
|
||||
|
||||
/* 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: "í"; }
|
||||
|
||||
/*[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;
|
||||
}
|
||||
}*/
|
||||
/* 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: "ì"; }
|
||||
.s-limit-red {
|
||||
background: rgba(255, 0, 0, 0.3) !important; }
|
||||
|
||||
/* 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: "î"; }
|
||||
.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.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 36, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-upr td:first-child:before {
|
||||
content: "ë"; }
|
||||
/* line 37, ../../../../general/res/sass/_limits.scss */
|
||||
tr[class*="s-limit"].s-limit-lwr td:first-child:before {
|
||||
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 48, ../../../../general/res/sass/_limits.scss */
|
||||
:not(tr)[class*="s-limit"].s-limit-upr:before {
|
||||
content: "ë"; }
|
||||
/* line 49, ../../../../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 {
|
||||
@ -4089,32 +4114,10 @@ span.req {
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
position: relative; }
|
||||
/* line 228, ../../../../general/res/sass/search/_search.scss */
|
||||
/* line 226, ../../../../general/res/sass/search/_search.scss */
|
||||
.search .search-scroll .load-icon {
|
||||
position: relative;
|
||||
/* &.loading {
|
||||
pointer-events: none;
|
||||
margin-left: $leftMargin;
|
||||
|
||||
.title-label {
|
||||
// Text styling
|
||||
font-style: italic;
|
||||
font-size: .9em;
|
||||
opacity: 0.5;
|
||||
|
||||
// Text positioning
|
||||
margin-left: $iconWidth + $leftMargin;
|
||||
line-height: 24px;
|
||||
}
|
||||
.wait-spinner {
|
||||
margin-left: $leftMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.loading) {
|
||||
cursor: pointer;
|
||||
}*/ }
|
||||
/* line 255, ../../../../general/res/sass/search/_search.scss */
|
||||
position: relative; }
|
||||
/* line 230, ../../../../general/res/sass/search/_search.scss */
|
||||
.search .search-scroll .load-more-button {
|
||||
margin-top: 5px 0;
|
||||
font-size: 0.8em;
|
||||
@ -4367,21 +4370,7 @@ ul.tree {
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 3px;
|
||||
position: relative;
|
||||
/*
|
||||
&.loading {
|
||||
pointer-events: none;
|
||||
.label {
|
||||
opacity: 0.5;
|
||||
.title-label {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
.wait-spinner {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
*/ }
|
||||
position: relative; }
|
||||
/* line 48, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .view-control,
|
||||
.search-result-item .view-control {
|
||||
@ -4427,7 +4416,7 @@ ul.tree {
|
||||
.search-result-item .label .type-icon .icon.l-icon-alert {
|
||||
position: absolute;
|
||||
z-index: 2; }
|
||||
/* line 90, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 89, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .type-icon .icon.l-icon-alert,
|
||||
.search-result-item .label .type-icon .icon.l-icon-alert {
|
||||
color: #ff3c00;
|
||||
@ -4437,7 +4426,7 @@ ul.tree {
|
||||
width: 8px;
|
||||
top: 1px;
|
||||
right: -2px; }
|
||||
/* line 96, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 95, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .type-icon .icon.l-icon-link,
|
||||
.search-result-item .label .type-icon .icon.l-icon-link {
|
||||
color: #49dedb;
|
||||
@ -4447,7 +4436,7 @@ ul.tree {
|
||||
width: 8px;
|
||||
left: -3px;
|
||||
bottom: 0px; }
|
||||
/* line 104, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 103, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label .title-label,
|
||||
.search-result-item .label .title-label {
|
||||
overflow: hidden;
|
||||
@ -4463,47 +4452,47 @@ ul.tree {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
/* line 130, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 113, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected,
|
||||
.search-result-item.selected {
|
||||
background: #1ac6ff;
|
||||
color: #fcfcfc; }
|
||||
/* line 133, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 116, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected .view-control,
|
||||
.search-result-item.selected .view-control {
|
||||
color: #fcfcfc; }
|
||||
/* line 136, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 119, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item.selected .label .type-icon,
|
||||
.search-result-item.selected .label .type-icon {
|
||||
color: #fcfcfc; }
|
||||
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
|
||||
/* line 144, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 127, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.selected):hover,
|
||||
.search-result-item:not(.selected):hover {
|
||||
background: rgba(102, 102, 102, 0.1);
|
||||
color: #333333; }
|
||||
/* line 150, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 130, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.selected):hover .icon,
|
||||
.search-result-item:not(.selected):hover .icon {
|
||||
color: #0099cc; } }
|
||||
/* line 157, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 137, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item:not(.loading),
|
||||
.search-result-item:not(.loading) {
|
||||
cursor: pointer; }
|
||||
/* line 161, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 141, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .context-trigger,
|
||||
.search-result-item .context-trigger {
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
right: 3px; }
|
||||
/* line 167, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 146, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .context-trigger .invoke-menu,
|
||||
.search-result-item .context-trigger .invoke-menu {
|
||||
font-size: 0.75em;
|
||||
height: 0.9rem;
|
||||
line-height: 0.9rem; }
|
||||
|
||||
/* line 176, ../../../../general/res/sass/tree/_tree.scss */
|
||||
/* line 155, ../../../../general/res/sass/tree/_tree.scss */
|
||||
.tree-item .label {
|
||||
left: 15px; }
|
||||
|
||||
|
@ -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
@ -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
@ -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",
|
||||
|
@ -36,9 +36,9 @@
|
||||
{
|
||||
"key": "TIME_CONDUCTOR_DOMAINS",
|
||||
"value": [
|
||||
{ "key": "time", "name": "Time" },
|
||||
{ "key": "yesterday", "name": "Yesterday" }
|
||||
{ "key": "time", "name": "UTC", "format": "utc" }
|
||||
],
|
||||
"priority": "fallback",
|
||||
"comment": "Placeholder; to be replaced by inspection of available domains."
|
||||
}
|
||||
]
|
||||
|
@ -1,4 +1,5 @@
|
||||
<mct-include key="'time-controller'"
|
||||
parameters='parameters'
|
||||
ng-model='ngModel.conductor'>
|
||||
</mct-include>
|
||||
<mct-control key="'select'"
|
||||
|
@ -27,7 +27,10 @@ define(
|
||||
"use strict";
|
||||
|
||||
var TEMPLATE = [
|
||||
"<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
|
||||
"<mct-include key=\"'time-conductor'\" ",
|
||||
"ng-model='ngModel' ",
|
||||
"parameters='parameters' ",
|
||||
"class='l-time-controller'>",
|
||||
"</mct-include>"
|
||||
].join(''),
|
||||
THROTTLE_MS = 200,
|
||||
@ -74,11 +77,11 @@ define(
|
||||
broadcastBounds;
|
||||
|
||||
// Combine start/end times into a single object
|
||||
function bounds(start, end) {
|
||||
function bounds() {
|
||||
return {
|
||||
start: conductor.displayStart(),
|
||||
end: conductor.displayEnd(),
|
||||
domain: conductor.domain()
|
||||
domain: conductor.domain().key
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,12 +100,10 @@ define(
|
||||
}
|
||||
|
||||
function updateDomain(value) {
|
||||
conductor.domain(value);
|
||||
repScope.$broadcast('telemetry:display:bounds', bounds(
|
||||
conductor.displayStart(),
|
||||
conductor.displayEnd(),
|
||||
conductor.domain()
|
||||
));
|
||||
var newDomain = conductor.domain(value);
|
||||
conductorScope.parameters.format =
|
||||
newDomain && newDomain.format;
|
||||
broadcastBounds();
|
||||
}
|
||||
|
||||
// telemetry domain metadata -> option for a select control
|
||||
@ -130,7 +131,8 @@ define(
|
||||
{ outer: bounds(), inner: bounds() };
|
||||
conductorScope.ngModel.options =
|
||||
conductor.domainOptions().map(makeOption);
|
||||
conductorScope.ngModel.domain = conductor.domain();
|
||||
conductorScope.ngModel.domain = conductor.domain().key;
|
||||
conductorScope.parameters = {};
|
||||
|
||||
conductorScope
|
||||
.$watch('ngModel.conductor.inner.start', updateConductorInner);
|
||||
@ -138,8 +140,6 @@ define(
|
||||
.$watch('ngModel.conductor.inner.end', updateConductorInner);
|
||||
conductorScope
|
||||
.$watch('ngModel.domain', updateDomain);
|
||||
|
||||
repScope.$on('telemetry:view', updateConductorInner);
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.conductorScope = function (s) {
|
||||
|
@ -51,7 +51,7 @@ define(
|
||||
request = request || {};
|
||||
request.start = start;
|
||||
request.end = end;
|
||||
request.domain = domain;
|
||||
request.domain = domain.key;
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ define(
|
||||
function TimeConductor(start, end, domains) {
|
||||
this.range = { start: start, end: end };
|
||||
this.domains = domains;
|
||||
this.activeDomain = domains[0].key;
|
||||
this.activeDomain = domains[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +73,7 @@ define(
|
||||
/**
|
||||
* Get available domain options which can be used to bound time
|
||||
* selection.
|
||||
* @returns {TelemetryDomain[]} available domains
|
||||
* @returns {TelemetryDomainMetadata[]} available domains
|
||||
*/
|
||||
TimeConductor.prototype.domainOptions = function () {
|
||||
return this.domains;
|
||||
@ -82,19 +82,21 @@ define(
|
||||
/**
|
||||
* Get or set (if called with an argument) the active domain.
|
||||
* @param {string} [key] the key identifying the domain choice
|
||||
* @returns {TelemetryDomain} the active telemetry domain
|
||||
* @returns {TelemetryDomainMetadata} the active telemetry domain
|
||||
*/
|
||||
TimeConductor.prototype.domain = function (key) {
|
||||
function matchesKey(domain) {
|
||||
return domain.key === key;
|
||||
}
|
||||
var i;
|
||||
|
||||
if (arguments.length > 0) {
|
||||
if (!this.domains.some(matchesKey)) {
|
||||
throw new Error("Unknown domain " + key);
|
||||
for (i = 0; i < this.domains.length; i += 1) {
|
||||
if (this.domains[i].key === key) {
|
||||
return (this.activeDomain = this.domains[i]);
|
||||
}
|
||||
}
|
||||
this.activeDomain = key;
|
||||
|
||||
throw new Error("Unknown domain " + key);
|
||||
}
|
||||
|
||||
return this.activeDomain;
|
||||
};
|
||||
|
||||
|
@ -129,7 +129,7 @@ define(
|
||||
it("exposes conductor state in scope", function () {
|
||||
mockConductor.displayStart.andReturn(1977);
|
||||
mockConductor.displayEnd.andReturn(1984);
|
||||
mockConductor.domain.andReturn('d');
|
||||
mockConductor.domain.andReturn({ key: 'd' });
|
||||
representer.represent(testViews[0], {});
|
||||
|
||||
expect(mockNewScope.ngModel.conductor).toEqual({
|
||||
@ -219,7 +219,7 @@ define(
|
||||
representer.represent(testViews[0], null);
|
||||
|
||||
expect(mockNewScope.ngModel.domain)
|
||||
.toEqual(mockConductor.domain());
|
||||
.toEqual(mockConductor.domain().key);
|
||||
});
|
||||
|
||||
it("exposes domain options in scope", function () {
|
||||
|
@ -77,7 +77,7 @@ define(
|
||||
|
||||
mockConductor.displayStart.andReturn(42);
|
||||
mockConductor.displayEnd.andReturn(1977);
|
||||
mockConductor.domain.andReturn("testDomain");
|
||||
mockConductor.domain.andReturn({ key: "testDomain" });
|
||||
|
||||
decorator = new ConductorTelemetryDecorator(
|
||||
mockConductorService,
|
||||
@ -104,7 +104,7 @@ define(
|
||||
});
|
||||
|
||||
it("with domain selection", function () {
|
||||
expect(request.domain).toEqual(mockConductor.domain());
|
||||
expect(request.domain).toEqual(mockConductor.domain().key);
|
||||
});
|
||||
});
|
||||
|
||||
@ -127,7 +127,7 @@ define(
|
||||
});
|
||||
|
||||
it("with domain selection", function () {
|
||||
expect(request.domain).toEqual(mockConductor.domain());
|
||||
expect(request.domain).toEqual(mockConductor.domain().key);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -59,12 +59,12 @@ define(
|
||||
});
|
||||
|
||||
it("exposes the current domain choice", function () {
|
||||
expect(conductor.domain()).toEqual(testDomains[0].key);
|
||||
expect(conductor.domain()).toEqual(testDomains[0]);
|
||||
});
|
||||
|
||||
it("allows the domain choice to be changed", function () {
|
||||
conductor.domain(testDomains[1].key);
|
||||
expect(conductor.domain()).toEqual(testDomains[1].key);
|
||||
expect(conductor.domain()).toEqual(testDomains[1]);
|
||||
});
|
||||
|
||||
it("throws an error on attempts to set an invalid domain", function () {
|
||||
|
@ -31,10 +31,19 @@ define(
|
||||
"./elements/PlotPalette",
|
||||
"./elements/PlotAxis",
|
||||
"./elements/PlotLimitTracker",
|
||||
"./elements/PlotTelemetryFormatter",
|
||||
"./modes/PlotModeOptions",
|
||||
"./SubPlotFactory"
|
||||
],
|
||||
function (PlotUpdater, PlotPalette, PlotAxis, PlotLimitTracker, PlotModeOptions, SubPlotFactory) {
|
||||
function (
|
||||
PlotUpdater,
|
||||
PlotPalette,
|
||||
PlotAxis,
|
||||
PlotLimitTracker,
|
||||
PlotTelemetryFormatter,
|
||||
PlotModeOptions,
|
||||
SubPlotFactory
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var AXIS_DEFAULTS = [
|
||||
@ -62,7 +71,10 @@ define(
|
||||
PLOT_FIXED_DURATION
|
||||
) {
|
||||
var self = this,
|
||||
subPlotFactory = new SubPlotFactory(telemetryFormatter),
|
||||
plotTelemetryFormatter =
|
||||
new PlotTelemetryFormatter(telemetryFormatter),
|
||||
subPlotFactory =
|
||||
new SubPlotFactory(plotTelemetryFormatter),
|
||||
cachedObjects = [],
|
||||
updater,
|
||||
lastBounds,
|
||||
@ -71,10 +83,9 @@ define(
|
||||
// Populate the scope with axis information (specifically, options
|
||||
// available for each axis.)
|
||||
function setupAxes(metadatas) {
|
||||
$scope.axes = [
|
||||
new PlotAxis("domain", metadatas, AXIS_DEFAULTS[0]),
|
||||
new PlotAxis("range", metadatas, AXIS_DEFAULTS[1])
|
||||
];
|
||||
$scope.axes.forEach(function (axis) {
|
||||
axis.updateMetadata(metadatas);
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger an update of a specific subplot;
|
||||
@ -125,37 +136,49 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdater() {
|
||||
if (!updater) {
|
||||
recreateUpdater();
|
||||
}
|
||||
return updater;
|
||||
}
|
||||
|
||||
// Handle new telemetry data in this plot
|
||||
function updateValues() {
|
||||
self.pending = false;
|
||||
if (handle) {
|
||||
setupModes(handle.getTelemetryObjects());
|
||||
}
|
||||
if (updater) {
|
||||
updater.update();
|
||||
setupAxes(handle.getMetadata());
|
||||
getUpdater().update();
|
||||
self.modeOptions.getModeHandler().plotTelemetry(updater);
|
||||
}
|
||||
if (self.limitTracker) {
|
||||
self.limitTracker.update();
|
||||
self.update();
|
||||
}
|
||||
self.update();
|
||||
}
|
||||
|
||||
// Display new historical data as it becomes available
|
||||
function addHistoricalData(domainObject, series) {
|
||||
self.pending = false;
|
||||
updater.addHistorical(domainObject, series);
|
||||
getUpdater().addHistorical(domainObject, series);
|
||||
self.modeOptions.getModeHandler().plotTelemetry(updater);
|
||||
self.update();
|
||||
}
|
||||
|
||||
// Issue a new request for historical telemetry
|
||||
function requestTelemetry() {
|
||||
if (handle && updater) {
|
||||
if (handle) {
|
||||
handle.request({}, addHistoricalData);
|
||||
}
|
||||
}
|
||||
|
||||
// Requery for data entirely
|
||||
function replot() {
|
||||
if (handle) {
|
||||
updater = undefined;
|
||||
requestTelemetry();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new subscription; telemetrySubscriber gets
|
||||
// to do the meaningful work here.
|
||||
function subscribe(domainObject) {
|
||||
@ -167,12 +190,7 @@ define(
|
||||
updateValues,
|
||||
true // Lossless
|
||||
);
|
||||
if (handle) {
|
||||
setupModes(handle.getTelemetryObjects());
|
||||
setupAxes(handle.getMetadata());
|
||||
recreateUpdater();
|
||||
requestTelemetry();
|
||||
}
|
||||
replot();
|
||||
}
|
||||
|
||||
// Release the current subscription (called when scope is destroyed)
|
||||
@ -185,12 +203,22 @@ define(
|
||||
|
||||
// Respond to a display bounds change (requery for data)
|
||||
function changeDisplayBounds(event, bounds) {
|
||||
var domainAxis = $scope.axes[0];
|
||||
|
||||
domainAxis.chooseOption(bounds.domain);
|
||||
plotTelemetryFormatter
|
||||
.setDomainFormat(domainAxis.active.format);
|
||||
|
||||
self.pending = true;
|
||||
releaseSubscription();
|
||||
subscribe($scope.domainObject);
|
||||
setBasePanZoom(bounds);
|
||||
}
|
||||
|
||||
function updateDomainFormat(format) {
|
||||
plotTelemetryFormatter.setDomainFormat(format);
|
||||
}
|
||||
|
||||
this.modeOptions = new PlotModeOptions([], subPlotFactory);
|
||||
this.updateValues = updateValues;
|
||||
|
||||
@ -202,6 +230,13 @@ define(
|
||||
|
||||
self.pending = true;
|
||||
|
||||
// Initialize axes; will get repopulated when telemetry
|
||||
// metadata becomes available.
|
||||
$scope.axes = [
|
||||
new PlotAxis("domains", [], AXIS_DEFAULTS[0]),
|
||||
new PlotAxis("ranges", [], AXIS_DEFAULTS[1])
|
||||
];
|
||||
|
||||
// Subscribe to telemetry when a domain object becomes available
|
||||
$scope.$watch('domainObject', subscribe);
|
||||
|
||||
|
@ -121,9 +121,9 @@ define(
|
||||
// Utility, for map/forEach loops. Index 0 is domain,
|
||||
// index 1 is range.
|
||||
function formatValue(v, i) {
|
||||
return (i ?
|
||||
formatter.formatRangeValue :
|
||||
formatter.formatDomainValue)(v);
|
||||
return i ?
|
||||
formatter.formatRangeValue(v) :
|
||||
formatter.formatDomainValue(v);
|
||||
}
|
||||
|
||||
this.hoverCoordinates = this.mousePosition &&
|
||||
|
@ -46,21 +46,9 @@ define(
|
||||
*
|
||||
*/
|
||||
function PlotAxis(axisType, metadatas, defaultValue) {
|
||||
var keys = {},
|
||||
options = [];
|
||||
|
||||
// Look through all metadata objects and assemble a list
|
||||
// of all possible domain or range options
|
||||
function buildOptionsForMetadata(m) {
|
||||
(m[axisType] || []).forEach(function (option) {
|
||||
if (!keys[option.key]) {
|
||||
keys[option.key] = true;
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(metadatas || []).forEach(buildOptionsForMetadata);
|
||||
this.axisType = axisType;
|
||||
this.defaultValue = defaultValue;
|
||||
this.optionKeys = {};
|
||||
|
||||
/**
|
||||
* The currently chosen option for this axis. An
|
||||
@ -68,7 +56,7 @@ define(
|
||||
* directly form the plot template.
|
||||
* @memberof platform/features/plot.PlotAxis#
|
||||
*/
|
||||
this.active = options[0] || defaultValue;
|
||||
this.active = defaultValue;
|
||||
|
||||
/**
|
||||
* The set of options applicable for this axis;
|
||||
@ -77,9 +65,71 @@ define(
|
||||
* human-readable names respectively)
|
||||
* @memberof platform/features/plot.PlotAxis#
|
||||
*/
|
||||
this.options = options;
|
||||
this.options = [];
|
||||
|
||||
// Initialize options from metadata objects
|
||||
this.updateMetadata(metadatas);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update axis options to reflect current metadata.
|
||||
* @param {TelemetryMetadata[]} metadata objects describing
|
||||
* applicable telemetry
|
||||
*/
|
||||
PlotAxis.prototype.updateMetadata = function (metadatas) {
|
||||
var axisType = this.axisType,
|
||||
optionKeys = this.optionKeys,
|
||||
newOptions = {},
|
||||
toAdd = [];
|
||||
|
||||
function isValid(option) {
|
||||
return option && optionKeys[option.key];
|
||||
}
|
||||
|
||||
metadatas.forEach(function (m) {
|
||||
(m[axisType] || []).forEach(function (option) {
|
||||
var key = option.key;
|
||||
if (!optionKeys[key] && !newOptions[key]) {
|
||||
toAdd.push(option);
|
||||
}
|
||||
newOptions[key] = true;
|
||||
});
|
||||
});
|
||||
|
||||
optionKeys = this.optionKeys = newOptions;
|
||||
|
||||
// General approach here is to avoid changing object
|
||||
// instances unless something has really changed, since
|
||||
// Angular is watching; don't want to trigger extra digests.
|
||||
if (!this.options.every(isValid)) {
|
||||
this.options = this.options.filter(isValid);
|
||||
}
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
this.options = this.options.concat(toAdd);
|
||||
}
|
||||
|
||||
if (!isValid(this.active)) {
|
||||
this.active = this.options[0] || this.defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the domain/range selection for this axis. If the
|
||||
* provided `key` is not recognized as an option, no change
|
||||
* will occur.
|
||||
* @param {string} key the identifier for the domain/range
|
||||
*/
|
||||
PlotAxis.prototype.chooseOption = function (key) {
|
||||
var self = this;
|
||||
this.options.forEach(function (option) {
|
||||
if (option.key === key) {
|
||||
self.active = option;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return PlotAxis;
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Wraps a `TelemetryFormatter` to provide formats for domain and
|
||||
* range values; provides a single place to track domain/range
|
||||
* formats within a plot, allowing other plot elements to simply
|
||||
* request that values be formatted.
|
||||
* @constructor
|
||||
* @memberof platform/features/plot
|
||||
* @implements {platform/telemetry.TelemetryFormatter}
|
||||
* @param {TelemetryFormatter} telemetryFormatter the formatter
|
||||
* to wrap.
|
||||
*/
|
||||
function PlotTelemetryFormatter(telemetryFormatter) {
|
||||
this.telemetryFormatter = telemetryFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the format to use for domain values.
|
||||
* @param {string} key the format's identifier
|
||||
*/
|
||||
PlotTelemetryFormatter.prototype.setDomainFormat = function (key) {
|
||||
this.domainFormat = key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Specify the format to use for range values.
|
||||
* @param {string} key the format's identifier
|
||||
*/
|
||||
PlotTelemetryFormatter.prototype.setRangeFormat = function (key) {
|
||||
this.rangeFormat = key;
|
||||
};
|
||||
|
||||
PlotTelemetryFormatter.prototype.formatDomainValue = function (value) {
|
||||
return this.telemetryFormatter
|
||||
.formatDomainValue(value, this.domainFormat);
|
||||
};
|
||||
|
||||
PlotTelemetryFormatter.prototype.formatRangeValue = function (value) {
|
||||
return this.telemetryFormatter
|
||||
.formatRangeValue(value, this.rangeFormat);
|
||||
};
|
||||
|
||||
return PlotTelemetryFormatter;
|
||||
}
|
||||
);
|
@ -43,6 +43,14 @@ define(
|
||||
this.formatter = formatter;
|
||||
}
|
||||
|
||||
// For phantomjs compatibility, for headless testing
|
||||
// (Function.prototype.bind unsupported)
|
||||
function bind(fn, thisObj) {
|
||||
return fn.bind ? fn.bind(thisObj) : function () {
|
||||
return fn.apply(thisObj, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// Generate ticks; interpolate from start up to
|
||||
// start + span in count steps, using the provided
|
||||
// formatter to represent each value.
|
||||
@ -72,7 +80,7 @@ define(
|
||||
panZoom.origin[0],
|
||||
panZoom.dimensions[0],
|
||||
count,
|
||||
this.formatter.formatDomainValue
|
||||
bind(this.formatter.formatDomainValue, this.formatter)
|
||||
);
|
||||
};
|
||||
|
||||
@ -87,7 +95,7 @@ define(
|
||||
panZoom.origin[1],
|
||||
panZoom.dimensions[1],
|
||||
count,
|
||||
this.formatter.formatRangeValue
|
||||
bind(this.formatter.formatRangeValue, this.formatter)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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];
|
||||
|
||||
|
@ -169,8 +169,9 @@ define(
|
||||
mockDomainObject
|
||||
]);
|
||||
|
||||
// Make an object available
|
||||
// Make an object available; invoke handler's callback
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
mockHandler.handle.mostRecentCall.args[1]();
|
||||
|
||||
expect(controller.getModeOptions().length).toEqual(1);
|
||||
|
||||
@ -181,8 +182,9 @@ define(
|
||||
mockDomainObject
|
||||
]);
|
||||
|
||||
// Make an object available
|
||||
// Make an object available; invoke handler's callback
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
mockHandler.handle.mostRecentCall.args[1]();
|
||||
|
||||
expect(controller.getModeOptions().length).toEqual(2);
|
||||
});
|
||||
|
@ -30,7 +30,12 @@ define(
|
||||
"use strict";
|
||||
|
||||
describe("A plot axis", function () {
|
||||
var testMetadatas = [
|
||||
var testMetadatas,
|
||||
testDefault,
|
||||
axis;
|
||||
|
||||
beforeEach(function () {
|
||||
testMetadatas = [
|
||||
{
|
||||
tests: [
|
||||
{ key: "t0", name: "T0" },
|
||||
@ -52,13 +57,14 @@ define(
|
||||
{ key: "t6", name: "T6" }
|
||||
]
|
||||
}
|
||||
],
|
||||
testDefault = { key: "test", name: "Test" },
|
||||
controller = new PlotAxis("tests", testMetadatas, testDefault);
|
||||
];
|
||||
testDefault = { key: "test", name: "Test" };
|
||||
axis = new PlotAxis("tests", testMetadatas, testDefault);
|
||||
});
|
||||
|
||||
it("pulls out a list of domain or range options", function () {
|
||||
// Should have filtered out duplicates, etc
|
||||
expect(controller.options).toEqual([
|
||||
expect(axis.options).toEqual([
|
||||
{ key: "t0", name: "T0" },
|
||||
{ key: "t1", name: "T1" },
|
||||
{ key: "t2", name: "T2" },
|
||||
@ -70,7 +76,7 @@ define(
|
||||
});
|
||||
|
||||
it("chooses the first option as a default", function () {
|
||||
expect(controller.active).toEqual({ key: "t0", name: "T0" });
|
||||
expect(axis.active).toEqual({ key: "t0", name: "T0" });
|
||||
});
|
||||
|
||||
it("falls back to a provided default if no options are present", function () {
|
||||
@ -78,6 +84,26 @@ define(
|
||||
.toEqual(testDefault);
|
||||
});
|
||||
|
||||
it("allows options to be chosen by key", function () {
|
||||
axis.chooseOption("t3");
|
||||
expect(axis.active).toEqual({ key: "t3", name: "T3" });
|
||||
});
|
||||
|
||||
it("reflects changes to applicable metadata", function () {
|
||||
axis.updateMetadata([ testMetadatas[1] ]);
|
||||
expect(axis.options).toEqual([
|
||||
{ key: "t0", name: "T0" },
|
||||
{ key: "t2", name: "T2" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns the same array instance for unchanged metadata", function () {
|
||||
// ...to avoid triggering extra digest cycles.
|
||||
var oldInstance = axis.options;
|
||||
axis.updateMetadata(testMetadatas);
|
||||
expect(axis.options).toBe(oldInstance);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/elements/PlotTelemetryFormatter"],
|
||||
function (PlotTelemetryFormatter) {
|
||||
'use strict';
|
||||
|
||||
describe("The PlotTelemetryFormatter", function () {
|
||||
var mockFormatter,
|
||||
formatter;
|
||||
|
||||
beforeEach(function () {
|
||||
mockFormatter = jasmine.createSpyObj(
|
||||
'telemetryFormatter',
|
||||
['formatDomainValue', 'formatRangeValue']
|
||||
);
|
||||
formatter = new PlotTelemetryFormatter(mockFormatter);
|
||||
});
|
||||
|
||||
describe("using domain & range format keys", function () {
|
||||
var rangeFormat = "someRangeFormat",
|
||||
domainFormat = "someDomainFormat";
|
||||
|
||||
beforeEach(function () {
|
||||
formatter.setRangeFormat(rangeFormat);
|
||||
formatter.setDomainFormat(domainFormat);
|
||||
});
|
||||
|
||||
it("includes format in formatDomainValue calls", function () {
|
||||
mockFormatter.formatDomainValue.andReturn("formatted!");
|
||||
expect(formatter.formatDomainValue(12321))
|
||||
.toEqual("formatted!");
|
||||
expect(mockFormatter.formatDomainValue)
|
||||
.toHaveBeenCalledWith(12321, domainFormat);
|
||||
});
|
||||
|
||||
it("includes format in formatRangeValue calls", function () {
|
||||
mockFormatter.formatRangeValue.andReturn("formatted!");
|
||||
expect(formatter.formatRangeValue(12321))
|
||||
.toEqual("formatted!");
|
||||
expect(mockFormatter.formatRangeValue)
|
||||
.toHaveBeenCalledWith(12321, rangeFormat);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -14,6 +14,7 @@
|
||||
"elements/PlotPosition",
|
||||
"elements/PlotPreparer",
|
||||
"elements/PlotSeriesWindow",
|
||||
"elements/PlotTelemetryFormatter",
|
||||
"elements/PlotTickGenerator",
|
||||
"elements/PlotUpdater",
|
||||
"modes/PlotModeOptions",
|
||||
|
@ -54,7 +54,8 @@ define(
|
||||
DomainColumn.prototype.getValue = function (domainObject, datum) {
|
||||
return {
|
||||
text: this.telemetryFormatter.formatDomainValue(
|
||||
datum[this.domainMetadata.key]
|
||||
datum[this.domainMetadata.key],
|
||||
this.domainMetadata.format
|
||||
)
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -4,12 +4,12 @@
|
||||
{
|
||||
"key": "mctInclude",
|
||||
"implementation": "MCTInclude.js",
|
||||
"depends": [ "templates[]", "$sce" ]
|
||||
"depends": [ "templates[]", "templateLinker" ]
|
||||
},
|
||||
{
|
||||
"key": "mctRepresentation",
|
||||
"implementation": "MCTRepresentation.js",
|
||||
"depends": [ "representations[]", "views[]", "representers[]", "$q", "$sce", "$log" ]
|
||||
"depends": [ "representations[]", "views[]", "representers[]", "$q", "templateLinker", "$log" ]
|
||||
}
|
||||
],
|
||||
"gestures": [
|
||||
@ -48,6 +48,12 @@
|
||||
"key": "dndService",
|
||||
"implementation": "services/DndService.js",
|
||||
"depends": [ "$log" ]
|
||||
},
|
||||
{
|
||||
"key": "templateLinker",
|
||||
"implementation": "TemplateLinker.js",
|
||||
"depends": [ "$templateRequest", "$sce", "$compile", "$log" ],
|
||||
"comment": "For internal use by mct-include and mct-representation."
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
|
@ -54,36 +54,38 @@ define(
|
||||
* @param {TemplateDefinition[]} templates an array of
|
||||
* template extensions
|
||||
*/
|
||||
function MCTInclude(templates, $sce) {
|
||||
function MCTInclude(templates, templateLinker) {
|
||||
var templateMap = {};
|
||||
|
||||
function link(scope, element) {
|
||||
var changeTemplate = templateLinker.link(
|
||||
scope,
|
||||
element,
|
||||
scope.key && templateMap[scope.key]
|
||||
);
|
||||
|
||||
scope.$watch('key', function (key) {
|
||||
changeTemplate(key && templateMap[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// Prepopulate templateMap for easy look up by key
|
||||
templates.forEach(function (template) {
|
||||
var key = template.key,
|
||||
path = $sce.trustAsResourceUrl([
|
||||
template.bundle.path,
|
||||
template.bundle.resources,
|
||||
template.templateUrl
|
||||
].join("/"));
|
||||
var key = template.key;
|
||||
// First found should win (priority ordering)
|
||||
templateMap[key] = templateMap[key] || path;
|
||||
templateMap[key] =
|
||||
templateMap[key] || templateLinker.getPath(template);
|
||||
});
|
||||
|
||||
function controller($scope) {
|
||||
// Pass the template URL to ng-include via scope.
|
||||
$scope.inclusion = templateMap[$scope.key];
|
||||
}
|
||||
|
||||
return {
|
||||
// Only show at the element level
|
||||
restrict: "E",
|
||||
|
||||
// Use the included controller to populate scope
|
||||
controller: controller,
|
||||
link: link,
|
||||
|
||||
// Use ng-include as a template; "inclusion" will be the real
|
||||
// template path
|
||||
template: '<ng-include src="inclusion"></ng-include>',
|
||||
// May hide the element, so let other directives act first
|
||||
priority: -1000,
|
||||
|
||||
// Two-way bind key, ngModel, and parameters
|
||||
scope: { key: "=", ngModel: "=", parameters: "=" }
|
||||
|
@ -55,7 +55,7 @@ define(
|
||||
* representation extensions
|
||||
* @param {ViewDefinition[]} views an array of view extensions
|
||||
*/
|
||||
function MCTRepresentation(representations, views, representers, $q, $sce, $log) {
|
||||
function MCTRepresentation(representations, views, representers, $q, templateLinker, $log) {
|
||||
var representationMap = {},
|
||||
gestureMap = {};
|
||||
|
||||
@ -72,11 +72,7 @@ define(
|
||||
|
||||
// Get a path to a representation
|
||||
function getPath(representation) {
|
||||
return $sce.trustAsResourceUrl([
|
||||
representation.bundle.path,
|
||||
representation.bundle.resources,
|
||||
representation.templateUrl
|
||||
].join("/"));
|
||||
return templateLinker.getPath(representation);
|
||||
}
|
||||
|
||||
// Look up a matching representation for this domain object
|
||||
@ -94,12 +90,13 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
function link($scope, element, attrs) {
|
||||
function link($scope, element, attrs, ctrl, transclude) {
|
||||
var activeRepresenters = representers.map(function (Representer) {
|
||||
return new Representer($scope, element, attrs);
|
||||
}),
|
||||
toClear = [], // Properties to clear out of scope on change
|
||||
counter = 0;
|
||||
counter = 0,
|
||||
changeTemplate = templateLinker.link($scope, element);
|
||||
|
||||
// Populate scope with any capabilities indicated by the
|
||||
// representation's extension definition
|
||||
@ -150,15 +147,17 @@ define(
|
||||
function refresh() {
|
||||
var domainObject = $scope.domainObject,
|
||||
representation = lookup($scope.key, domainObject),
|
||||
uses = ((representation || {}).uses || []);
|
||||
path = representation && getPath(representation),
|
||||
uses = ((representation || {}).uses || []),
|
||||
canRepresent = !!(path && domainObject);
|
||||
|
||||
// Create an empty object named "representation", for this
|
||||
// representation to store local variables into.
|
||||
$scope.representation = {};
|
||||
|
||||
// Look up the actual template path, pass it to ng-include
|
||||
// via the "inclusion" field
|
||||
$scope.inclusion = representation && getPath(representation);
|
||||
// Change templates (passing in undefined to clear
|
||||
// if we don't have enough info to show a template.)
|
||||
changeTemplate(canRepresent ? path : undefined);
|
||||
|
||||
// Any existing representers are no longer valid; release them.
|
||||
destroyRepresenters();
|
||||
@ -176,7 +175,7 @@ define(
|
||||
|
||||
// Populate scope with fields associated with the current
|
||||
// domain object (if one has been passed in)
|
||||
if (domainObject) {
|
||||
if (canRepresent) {
|
||||
// Track how many representations we've made in this scope,
|
||||
// to ensure that the correct representations are matched to
|
||||
// the correct object/key pairs.
|
||||
@ -233,9 +232,8 @@ define(
|
||||
// Handle Angular's linking step
|
||||
link: link,
|
||||
|
||||
// Use ng-include as a template; "inclusion" will be the real
|
||||
// template path
|
||||
template: '<ng-include src="inclusion"></ng-include>',
|
||||
// May hide the element, so let other directives act first
|
||||
priority: -1000,
|
||||
|
||||
// Two-way bind key and parameters, get the represented domain
|
||||
// object as "mct-object"
|
||||
|
164
platform/representation/src/TemplateLinker.js
Normal file
@ -0,0 +1,164 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The `templateLinker` service is intended for internal use by
|
||||
* the `mct-include` and `mct-representation` directives. It is
|
||||
* used to support common behavior of directives; specifically,
|
||||
* loading templates and inserting them into a specified element,
|
||||
* and/or removing that element from the DOM when there is no
|
||||
* template to populate it with.
|
||||
*
|
||||
* @param {Function} $templateRequest Angular's `$templateRequest`
|
||||
* service
|
||||
* @param $sce Angular's `$sce` service
|
||||
* @param {Function} $compile Angular's `$compile` service
|
||||
* @param $log Angular's `$log` service
|
||||
* @private
|
||||
*/
|
||||
function TemplateLinker($templateRequest, $sce, $compile, $log) {
|
||||
this.$templateRequest = $templateRequest;
|
||||
this.$sce = $sce;
|
||||
this.$compile = $compile;
|
||||
this.$log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a template from the given URL. This request will be handled
|
||||
* via `$templateRequest` to ensure caching et cetera.
|
||||
* @param {string} the URL for the template
|
||||
* @returns {Promise.<string>} a promise for the HTML content of
|
||||
* the template
|
||||
* @private
|
||||
*/
|
||||
TemplateLinker.prototype.load = function (templateUrl) {
|
||||
return this.$templateRequest(
|
||||
this.$sce.trustAsResourceUrl(templateUrl),
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a path to a template from an extension definition fo
|
||||
* a template, representation, or view.
|
||||
* @param {TemplateDefinition} extensionDefinition the definition
|
||||
* of the template/representation/view to resolve
|
||||
*/
|
||||
TemplateLinker.prototype.getPath = function (extensionDefinition) {
|
||||
return [
|
||||
extensionDefinition.bundle.path,
|
||||
extensionDefinition.bundle.resources,
|
||||
extensionDefinition.templateUrl
|
||||
].join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate the given element with templates, within the given scope;
|
||||
* intended to support the `link` function of the supported directives.
|
||||
*
|
||||
* @param {Scope} scope the Angular scope to use when rendering
|
||||
* templates
|
||||
* @param element the jqLite-wrapped element into which templates
|
||||
* should be inserted
|
||||
* @returns {Function} a function which can be called with a template
|
||||
* URL to switch templates, or `undefined` to remove.
|
||||
*/
|
||||
TemplateLinker.prototype.link = function (scope, element, templateUrl) {
|
||||
var activeElement = element,
|
||||
activeTemplateUrl,
|
||||
comment = this.$compile('<!-- hidden mct element -->')(scope),
|
||||
activeScope,
|
||||
self = this;
|
||||
|
||||
function destroyScope() {
|
||||
if (activeScope) {
|
||||
activeScope.$destroy();
|
||||
activeScope = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function removeElement() {
|
||||
if (activeElement !== comment) {
|
||||
destroyScope();
|
||||
activeElement.replaceWith(comment);
|
||||
activeElement = comment;
|
||||
}
|
||||
}
|
||||
|
||||
function addElement() {
|
||||
if (activeElement !== element) {
|
||||
activeElement.replaceWith(element);
|
||||
activeElement = element;
|
||||
activeElement.empty();
|
||||
}
|
||||
}
|
||||
|
||||
function populateElement(template) {
|
||||
destroyScope();
|
||||
activeScope = scope.$new(false);
|
||||
element.empty();
|
||||
element.append(self.$compile(template)(activeScope));
|
||||
}
|
||||
|
||||
function badTemplate(templateUrl) {
|
||||
self.$log.warn("Couldn't load template at " + templateUrl);
|
||||
removeElement();
|
||||
}
|
||||
|
||||
function changeTemplate(templateUrl) {
|
||||
if (templateUrl !== activeTemplateUrl) {
|
||||
if (templateUrl) {
|
||||
addElement();
|
||||
self.load(templateUrl).then(function (template) {
|
||||
// Avoid race conditions
|
||||
if (templateUrl === activeTemplateUrl) {
|
||||
populateElement(template);
|
||||
}
|
||||
}, function () {
|
||||
badTemplate(templateUrl);
|
||||
});
|
||||
} else {
|
||||
removeElement();
|
||||
}
|
||||
activeTemplateUrl = templateUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (templateUrl) {
|
||||
changeTemplate(templateUrl);
|
||||
} else {
|
||||
removeElement();
|
||||
}
|
||||
|
||||
return changeTemplate;
|
||||
};
|
||||
|
||||
return TemplateLinker;
|
||||
}
|
||||
);
|
||||
|
@ -31,9 +31,21 @@ define(
|
||||
|
||||
describe("The mct-include directive", function () {
|
||||
var testTemplates,
|
||||
mockSce,
|
||||
testUrls,
|
||||
mockLinker,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockChangeTemplate,
|
||||
mctInclude;
|
||||
|
||||
function fireWatch(expr, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
testTemplates = [
|
||||
{
|
||||
@ -47,40 +59,44 @@ define(
|
||||
templateUrl: "z/template.html"
|
||||
}
|
||||
];
|
||||
mockSce = jasmine.createSpyObj(
|
||||
'$sce',
|
||||
['trustAsResourceUrl']
|
||||
);
|
||||
mockSce.trustAsResourceUrl.andCallFake(function (url) {
|
||||
return url;
|
||||
testUrls = {};
|
||||
testTemplates.forEach(function (t, i) {
|
||||
testUrls[t.key] = "some URL " + String(i);
|
||||
});
|
||||
mctInclude = new MCTInclude(testTemplates, mockSce);
|
||||
});
|
||||
|
||||
it("has a built-in template, with ng-include src=inclusion", function () {
|
||||
// Not rigorous, but should detect many cases when template is broken.
|
||||
expect(mctInclude.template.indexOf("ng-include")).not.toEqual(-1);
|
||||
expect(mctInclude.template.indexOf("inclusion")).not.toEqual(-1);
|
||||
mockLinker = jasmine.createSpyObj(
|
||||
'templateLinker',
|
||||
['link', 'getPath']
|
||||
);
|
||||
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
|
||||
mockElement = jasmine.createSpyObj('element', ['empty']);
|
||||
mockChangeTemplate = jasmine.createSpy('changeTemplate');
|
||||
mockLinker.link.andReturn(mockChangeTemplate);
|
||||
mockLinker.getPath.andCallFake(function (template) {
|
||||
return testUrls[template.key];
|
||||
});
|
||||
mctInclude = new MCTInclude(testTemplates, mockLinker);
|
||||
mctInclude.link(mockScope, mockElement, {});
|
||||
});
|
||||
|
||||
it("is restricted to elements", function () {
|
||||
expect(mctInclude.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("reads a template location from a scope's key variable", function () {
|
||||
var scope = { key: "abc" };
|
||||
mctInclude.controller(scope);
|
||||
expect(scope.inclusion).toEqual("a/b/c/template.html");
|
||||
|
||||
scope = { key: "xyz" };
|
||||
mctInclude.controller(scope);
|
||||
expect(scope.inclusion).toEqual("x/y/z/template.html");
|
||||
it("exposes templates via the templateLinker", function () {
|
||||
expect(mockLinker.link)
|
||||
.toHaveBeenCalledWith(mockScope, mockElement, undefined);
|
||||
});
|
||||
|
||||
it("trusts template URLs", function () {
|
||||
mctInclude.controller({ key: "xyz" });
|
||||
expect(mockSce.trustAsResourceUrl)
|
||||
.toHaveBeenCalledWith("x/y/z/template.html");
|
||||
it("reads a template location from a scope's key variable", function () {
|
||||
mockScope.key = 'abc';
|
||||
fireWatch('key', mockScope.key);
|
||||
expect(mockChangeTemplate)
|
||||
.toHaveBeenCalledWith(testUrls.abc);
|
||||
|
||||
mockScope.key = 'xyz';
|
||||
fireWatch('key', mockScope.key);
|
||||
expect(mockChangeTemplate)
|
||||
.toHaveBeenCalledWith(testUrls.xyz);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -36,10 +36,12 @@ define(
|
||||
describe("The mct-representation directive", function () {
|
||||
var testRepresentations,
|
||||
testViews,
|
||||
testUrls,
|
||||
mockRepresenters,
|
||||
mockQ,
|
||||
mockSce,
|
||||
mockLinker,
|
||||
mockLog,
|
||||
mockChangeTemplate,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockDomainObject,
|
||||
@ -54,7 +56,17 @@ define(
|
||||
};
|
||||
}
|
||||
|
||||
function fireWatch(expr, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
testUrls = {};
|
||||
|
||||
testRepresentations = [
|
||||
{
|
||||
key: "abc",
|
||||
@ -85,6 +97,11 @@ define(
|
||||
|
||||
testModel = { someKey: "some value" };
|
||||
|
||||
testUrls = {};
|
||||
testViews.concat(testRepresentations).forEach(function (t, i) {
|
||||
testUrls[t.key] = "some URL " + String(i);
|
||||
});
|
||||
|
||||
mockRepresenters = ["A", "B"].map(function (name) {
|
||||
var constructor = jasmine.createSpy("Representer" + name),
|
||||
representer = jasmine.createSpyObj(
|
||||
@ -96,45 +113,44 @@ define(
|
||||
});
|
||||
|
||||
mockQ = { when: mockPromise };
|
||||
mockSce = jasmine.createSpyObj(
|
||||
'$sce',
|
||||
['trustAsResourceUrl']
|
||||
mockLinker = jasmine.createSpyObj(
|
||||
'templateLinker',
|
||||
['link', 'getPath']
|
||||
);
|
||||
mockChangeTemplate = jasmine.createSpy('changeTemplate');
|
||||
mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS);
|
||||
|
||||
|
||||
mockSce.trustAsResourceUrl.andCallFake(function (url) {
|
||||
return url;
|
||||
});
|
||||
mockScope = jasmine.createSpyObj("scope", [ "$watch", "$on" ]);
|
||||
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
|
||||
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
mockLinker.link.andReturn(mockChangeTemplate);
|
||||
mockLinker.getPath.andCallFake(function (ext) {
|
||||
return testUrls[ext.key];
|
||||
});
|
||||
|
||||
mctRepresentation = new MCTRepresentation(
|
||||
testRepresentations,
|
||||
testViews,
|
||||
mockRepresenters,
|
||||
mockQ,
|
||||
mockSce,
|
||||
mockLinker,
|
||||
mockLog
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("has a built-in template, with ng-include src=inclusion", function () {
|
||||
// Not rigorous, but should detect many cases when template is broken.
|
||||
expect(mctRepresentation.template.indexOf("ng-include")).not.toEqual(-1);
|
||||
expect(mctRepresentation.template.indexOf("inclusion")).not.toEqual(-1);
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
});
|
||||
|
||||
it("is restricted to elements", function () {
|
||||
expect(mctRepresentation.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("exposes templates via the templateLinker", function () {
|
||||
expect(mockLinker.link)
|
||||
.toHaveBeenCalledWith(mockScope, mockElement);
|
||||
});
|
||||
|
||||
it("watches scope when linked", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"key",
|
||||
jasmine.any(Function)
|
||||
@ -150,42 +166,46 @@ define(
|
||||
});
|
||||
|
||||
it("recognizes keys for representations", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "abc";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Trigger the watch
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
fireWatch('key', mockScope.key);
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(mockScope.inclusion).toEqual("a/b/c/template.html");
|
||||
expect(mockChangeTemplate)
|
||||
.toHaveBeenCalledWith(testUrls.abc);
|
||||
});
|
||||
|
||||
it("recognizes keys for views", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "xyz";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Trigger the watch
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
// Trigger the watches
|
||||
fireWatch('key', mockScope.key);
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(mockScope.inclusion).toEqual("x/y/z/template.html");
|
||||
expect(mockChangeTemplate)
|
||||
.toHaveBeenCalledWith(testUrls.xyz);
|
||||
});
|
||||
|
||||
it("trusts template URLs", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
it("does not load templates until there is an object", function () {
|
||||
mockScope.key = "xyz";
|
||||
|
||||
// Trigger the watch
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
fireWatch('key', mockScope.key);
|
||||
|
||||
expect(mockSce.trustAsResourceUrl)
|
||||
.toHaveBeenCalledWith("x/y/z/template.html");
|
||||
expect(mockChangeTemplate)
|
||||
.not.toHaveBeenCalledWith(jasmine.any(String));
|
||||
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(mockChangeTemplate)
|
||||
.toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
|
||||
it("loads declared capabilities", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "def";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
@ -199,8 +219,6 @@ define(
|
||||
});
|
||||
|
||||
it("logs when no representation is available for a key", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "someUnknownThing";
|
||||
|
||||
// Verify precondition
|
||||
@ -214,8 +232,6 @@ define(
|
||||
});
|
||||
|
||||
it("clears out obsolete peroperties from scope", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "def";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockDomainObject.useCapability.andReturn("some value");
|
||||
|
211
platform/representation/test/TemplateLinkerSpec.js
Normal file
@ -0,0 +1,211 @@
|
||||
/*****************************************************************************
|
||||
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
|
||||
define(
|
||||
["../src/TemplateLinker"],
|
||||
function (TemplateLinker) {
|
||||
'use strict';
|
||||
|
||||
var JQLITE_METHODS = [ 'replaceWith', 'empty', 'append' ];
|
||||
|
||||
describe("TemplateLinker", function () {
|
||||
var mockTemplateRequest,
|
||||
mockSce,
|
||||
mockCompile,
|
||||
mockLog,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockTemplates,
|
||||
mockElements,
|
||||
mockPromise,
|
||||
linker;
|
||||
|
||||
beforeEach(function () {
|
||||
mockTemplateRequest = jasmine.createSpy('$templateRequest');
|
||||
mockSce = jasmine.createSpyObj('$sce', ['trustAsResourceUrl']);
|
||||
mockCompile = jasmine.createSpy('$compile');
|
||||
mockLog = jasmine.createSpyObj('$log', ['error', 'warn']);
|
||||
mockScope = jasmine.createSpyObj('$scope', ['$on']);
|
||||
mockElement = jasmine.createSpyObj('element', JQLITE_METHODS);
|
||||
mockPromise = jasmine.createSpyObj('promise', ['then']);
|
||||
mockTemplates = {};
|
||||
mockElements = {};
|
||||
|
||||
mockTemplateRequest.andReturn(mockPromise);
|
||||
mockCompile.andCallFake(function (html) {
|
||||
mockTemplates[html] = jasmine.createSpy('template');
|
||||
mockElements[html] =
|
||||
jasmine.createSpyObj('templateEl', JQLITE_METHODS);
|
||||
mockTemplates[html].andReturn(mockElements[html]);
|
||||
return mockTemplates[html];
|
||||
});
|
||||
mockSce.trustAsResourceUrl.andCallFake(function (url) {
|
||||
return { trusted: url };
|
||||
});
|
||||
|
||||
linker = new TemplateLinker(
|
||||
mockTemplateRequest,
|
||||
mockSce,
|
||||
mockCompile,
|
||||
mockLog
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves extension paths", function () {
|
||||
expect(linker.getPath({
|
||||
bundle: {
|
||||
path: 'a',
|
||||
resources: 'b'
|
||||
},
|
||||
templateUrl: 'c/d.html'
|
||||
})).toEqual('a/b/c/d.html');
|
||||
});
|
||||
|
||||
describe("when linking elements", function () {
|
||||
var changeTemplate,
|
||||
commentElement;
|
||||
|
||||
function findCommentElement() {
|
||||
mockCompile.calls.forEach(function (call) {
|
||||
var html = call.args[0];
|
||||
if (html.indexOf("<!--") > -1) {
|
||||
commentElement = mockElements[html];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
changeTemplate = linker.link(mockScope, mockElement);
|
||||
findCommentElement();
|
||||
});
|
||||
|
||||
it("compiles a comment to use to replace element", function () {
|
||||
expect(commentElement).toBeDefined();
|
||||
});
|
||||
|
||||
it("initially replaces elements with comments", function () {
|
||||
expect(mockElement.replaceWith)
|
||||
.toHaveBeenCalledWith(commentElement);
|
||||
});
|
||||
|
||||
it("provides a function to change templates", function () {
|
||||
expect(changeTemplate).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe("and then changing templates", function () {
|
||||
var testUrl,
|
||||
testTemplate;
|
||||
|
||||
beforeEach(function () {
|
||||
testUrl = "some/url/template.html";
|
||||
testTemplate = "<div>Some template!</div>";
|
||||
changeTemplate(testUrl);
|
||||
mockPromise.then.mostRecentCall
|
||||
.args[0](testTemplate);
|
||||
});
|
||||
|
||||
it("loads templates using $templateRequest", function () {
|
||||
expect(mockTemplateRequest).toHaveBeenCalledWith({
|
||||
trusted: testUrl
|
||||
}, false);
|
||||
});
|
||||
|
||||
it("compiles loaded templates with linked scope", function () {
|
||||
expect(mockCompile).toHaveBeenCalledWith(testTemplate);
|
||||
expect(mockTemplates[testTemplate])
|
||||
.toHaveBeenCalledWith(mockScope);
|
||||
});
|
||||
|
||||
it("replaces comments with specified element", function () {
|
||||
expect(commentElement.replaceWith)
|
||||
.toHaveBeenCalledWith(mockElement);
|
||||
});
|
||||
|
||||
it("appends rendered content to the specified element", function () {
|
||||
expect(mockElement.append)
|
||||
.toHaveBeenCalledWith(mockElements[testTemplate]);
|
||||
});
|
||||
|
||||
it("clears templates when called with undefined", function () {
|
||||
expect(mockElement.replaceWith.callCount)
|
||||
.toEqual(1);
|
||||
changeTemplate(undefined);
|
||||
expect(mockElement.replaceWith.callCount)
|
||||
.toEqual(2);
|
||||
expect(mockElement.replaceWith.mostRecentCall.args[0])
|
||||
.toEqual(commentElement);
|
||||
});
|
||||
|
||||
it("logs no warnings for nominal changes", function () {
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("which cannot be found", function () {
|
||||
beforeEach(function () {
|
||||
changeTemplate("some/bad/url");
|
||||
// Reject the template promise
|
||||
mockPromise.then.mostRecentCall.args[1]();
|
||||
});
|
||||
|
||||
it("removes the element from the DOM", function () {
|
||||
expect(mockElement.replaceWith.callCount)
|
||||
.toEqual(2);
|
||||
expect(
|
||||
mockElement.replaceWith.mostRecentCall.args[0]
|
||||
).toEqual(commentElement);
|
||||
});
|
||||
|
||||
it("logs a warning", function () {
|
||||
expect(mockLog.warn)
|
||||
.toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("when an initial template URL is provided", function () {
|
||||
var testUrl;
|
||||
|
||||
beforeEach(function () {
|
||||
testUrl = "some/test/url.html";
|
||||
linker.link(mockScope, mockElement, testUrl);
|
||||
});
|
||||
|
||||
it("does not remove the element initially", function () {
|
||||
expect(mockElement.replaceWith)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads the specified template", function () {
|
||||
expect(mockTemplateRequest).toHaveBeenCalledWith({
|
||||
trusted: testUrl
|
||||
}, false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -7,5 +7,6 @@
|
||||
"gestures/GestureRepresenter",
|
||||
"services/DndService",
|
||||
"MCTInclude",
|
||||
"MCTRepresentation"
|
||||
]
|
||||
"MCTRepresentation",
|
||||
"TemplateLinker"
|
||||
]
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -37,7 +37,8 @@
|
||||
"services": [
|
||||
{
|
||||
"key": "telemetryFormatter",
|
||||
"implementation": "TelemetryFormatter.js"
|
||||
"implementation": "TelemetryFormatter.js",
|
||||
"depends": [ "formatService", "DEFAULT_TIME_FORMAT", "$log" ]
|
||||
},
|
||||
{
|
||||
"key": "telemetrySubscriber",
|
||||
@ -63,4 +64,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,64 @@ define(
|
||||
getRangeValue: ZERO
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides metadata about telemetry associated with a
|
||||
* given domain object.
|
||||
*
|
||||
* @typedef TelemetryMetadata
|
||||
* @property {string} source the machine-readable identifier for
|
||||
* the source of telemetry data for this object; used by
|
||||
* {@link TelemetryService} implementations to determine
|
||||
* whether or not they provide data for this object.
|
||||
* @property {string} key the machine-readable identifier for
|
||||
* telemetry data associated with this specific object,
|
||||
* within that `source`.
|
||||
* @property {TelemetryDomainMetadata[]} domains supported domain
|
||||
* options for telemetry data associated with this object,
|
||||
* to use in interpreting a {@link TelemetrySeries}
|
||||
* @property {TelemetryRangeMetadata[]} ranges supported range
|
||||
* options for telemetry data associated with this object,
|
||||
* to use in interpreting a {@link TelemetrySeries}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides metadata about range options within a telemetry series.
|
||||
* Range options describe distinct properties within any given datum
|
||||
* of a telemetry series; for instance, a telemetry series containing
|
||||
* both raw and uncalibrated values may provide separate ranges for
|
||||
* each.
|
||||
*
|
||||
* @typedef TelemetryRangeMetadata
|
||||
* @property {string} key machine-readable identifier for this range
|
||||
* @property {string} name human-readable name for this range
|
||||
* @property {string} [units] human-readable units for this range
|
||||
* @property {string} [format] data format for this range; usually,
|
||||
* one of `number`, or `string`. If `undefined`,
|
||||
* should presume to be a `number`. Custom formats
|
||||
* may be indicated here.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides metadata about domain options within a telemetry series.
|
||||
* Domain options describe distinct properties within any given datum
|
||||
* of a telemtry series; for instance, a telemetry series containing
|
||||
* both spacecraft event time and earth received times may provide
|
||||
* separate domains for each.
|
||||
*
|
||||
* Domains are typically used to represent timestamps in a telemetry
|
||||
* series, but more generally may express any property which will
|
||||
* have unique values for each datum in a series. It is this property
|
||||
* which makes domains distinct from ranges, as it makes these values
|
||||
* appropriate and meaningful for use to sort and bound a series.
|
||||
*
|
||||
* @typedef TelemetryDomainMetadata
|
||||
* @property {string} key machine-readable identifier for this range
|
||||
* @property {string} name human-readable name for this range
|
||||
* @property {string} [system] machine-readable identifier for the
|
||||
* time/date system associated with this domain;
|
||||
* used by {@link DateService}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A telemetry capability provides a means of requesting telemetry
|
||||
* for a specific object, and for unwrapping the response (to get
|
||||
|
@ -22,14 +22,13 @@
|
||||
/*global define,moment*/
|
||||
|
||||
define(
|
||||
['moment'],
|
||||
function (moment) {
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// Date format to use for domain values; in particular,
|
||||
// use day-of-year instead of month/day
|
||||
var DATE_FORMAT = "YYYY-DDD HH:mm:ss",
|
||||
VALUE_FORMAT_DIGITS = 3;
|
||||
var VALUE_FORMAT_DIGITS = 3;
|
||||
|
||||
/**
|
||||
* The TelemetryFormatter is responsible for formatting (as text
|
||||
@ -37,22 +36,33 @@ define(
|
||||
* the range (usually value) of a data series.
|
||||
* @memberof platform/telemetry
|
||||
* @constructor
|
||||
* @param {FormatService} formatService the service to user to format
|
||||
* domain values
|
||||
* @param {string} defaultFormatKey the format to request when no
|
||||
* format has been otherwise specified
|
||||
* @param $log Angular's `$log`, to log warnings
|
||||
*/
|
||||
function TelemetryFormatter() {
|
||||
function TelemetryFormatter(formatService, defaultFormatKey, $log) {
|
||||
this.formatService = formatService;
|
||||
this.defaultFormat = formatService.getFormat(defaultFormatKey);
|
||||
this.$log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a domain value.
|
||||
* @param {number} v the domain value; a timestamp
|
||||
* @param {number} v the domain value; usually, a timestamp
|
||||
* in milliseconds since start of 1970
|
||||
* @param {string} [key] the key which identifies the
|
||||
* domain; if unspecified or unknown, this will
|
||||
* be treated as a standard timestamp.
|
||||
* @param {string} [key] a key which identifies the format
|
||||
* to use
|
||||
* @returns {string} a textual representation of the
|
||||
* data and time, suitable for display.
|
||||
*/
|
||||
TelemetryFormatter.prototype.formatDomainValue = function (v, key) {
|
||||
return isNaN(v) ? "" : moment.utc(v).format(DATE_FORMAT);
|
||||
var formatter = (key === undefined) ?
|
||||
this.defaultFormat :
|
||||
this.formatService.getFormat(key);
|
||||
|
||||
return isNaN(v) ? "" : formatter ? formatter.format(v) : String(v);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -27,16 +27,35 @@ define(
|
||||
"use strict";
|
||||
|
||||
describe("The telemetry formatter", function () {
|
||||
var formatter;
|
||||
var mockFormatService,
|
||||
mockFormat,
|
||||
formatter;
|
||||
|
||||
beforeEach(function () {
|
||||
formatter = new TelemetryFormatter();
|
||||
mockFormatService =
|
||||
jasmine.createSpyObj("formatService", ["getFormat"]);
|
||||
mockFormat = jasmine.createSpyObj("format", [
|
||||
"validate",
|
||||
"parse",
|
||||
"format"
|
||||
]);
|
||||
mockFormatService.getFormat.andReturn(mockFormat);
|
||||
formatter = new TelemetryFormatter(mockFormatService);
|
||||
});
|
||||
|
||||
it("formats domains using YYYY-DDD style", function () {
|
||||
expect(formatter.formatDomainValue(402513731000)).toEqual(
|
||||
"1982-276 17:22:11"
|
||||
);
|
||||
it("formats domains using the formatService", function () {
|
||||
var testValue = 12321, testResult = "some result";
|
||||
mockFormat.format.andReturn(testResult);
|
||||
|
||||
expect(formatter.formatDomainValue(testValue))
|
||||
.toEqual(testResult);
|
||||
expect(mockFormat.format).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
|
||||
it("passes format keys to the formatService", function () {
|
||||
formatter.formatDomainValue(12321, "someKey");
|
||||
expect(mockFormatService.getFormat)
|
||||
.toHaveBeenCalledWith("someKey");
|
||||
});
|
||||
|
||||
it("formats ranges as values", function () {
|
||||
@ -44,4 +63,4 @@ define(
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
|