Merge remote-tracking branch 'origin/wtd575' into open-master

Conflicts:
	bundles.json
This commit is contained in:
bwyu 2014-12-05 00:44:22 -08:00
commit 505b6ee241
10 changed files with 954 additions and 2 deletions

View File

@ -6,7 +6,7 @@
"platform/commonUI/edit",
"platform/commonUI/dialog",
"platform/commonUI/general",
"platform/forms",
"platform/telemetry",
"example/persistence"
]

View File

@ -0,0 +1,2 @@
This bundle is responsible for introducing a reusable infrastructure
and set of APIs for using time-series data in Open MCT Web.

View File

@ -0,0 +1,28 @@
{
"name": "Data bundle",
"description": "Interfaces and infrastructure for real-time and historical data.",
"extensions": {
"components": [
{
"provides": "telemetryService",
"type": "aggregator",
"implementation": "TelemetryAggregator.js",
"depends": [ "$q" ]
}
],
"controllers": [
{
"key": "TelemetryController",
"implementation": "TelemetryController.js",
"depends": [ "$scope", "$q", "$timeout", "$log" ]
}
],
"capabilities": [
{
"key": "telemetry",
"implementation": "TelemetryCapability.js",
"depends": [ "$injector", "$q", "$log" ]
}
]
}
}

View File

@ -0,0 +1,56 @@
/*global define*/
/**
* Module defining TelemetryProvider. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
"use strict";
/**
* A telemetry aggregator makes many telemetry providers
* appear as one.
*
* @constructor
*/
function TelemetryAggregator($q, telemetryProviders) {
// Merge the results from many providers into one
// result object.
function mergeResults(results) {
var merged = {};
results.forEach(function (result) {
Object.keys(result).forEach(function (k) {
merged[k] = result[k];
});
});
return merged;
}
// Request telemetry from all providers; once they've
// responded, merge the results into one result object.
function requestTelemetry(requests) {
return $q.all(telemetryProviders.map(function (provider) {
return provider.requestTelemetry(requests);
})).then(mergeResults);
}
return {
/**
* Request telemetry data.
* @param {TelemetryRequest[]} requests and array of
* requests to be handled
* @returns {Promise} a promise for telemetry data
* which may (or may not, depending on
* availability) satisfy the requests
*/
requestTelemetry: requestTelemetry
};
}
return TelemetryAggregator;
}
);

View File

@ -0,0 +1,133 @@
/*global define*/
/**
* Module defining TelemetryCapability. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
"use strict";
/**
* A telemetry capability provides a means of requesting telemetry
* for a specific object, and for unwrapping the response (to get
* at the specific data which is appropriate to the domain object.)
*
* @constructor
*/
function TelemetryCapability($injector, $q, $log, domainObject) {
var telemetryService;
// We could depend on telemetryService directly, but
// there isn't a platform implementation of this;
function getTelemetryService() {
if (!telemetryService) {
try {
telemetryService =
$q.when($injector.get("telemetryService"));
} catch (e) {
// $injector should throw is telemetryService
// is unavailable or unsatisfiable.
$log.warn("Telemetry service unavailable");
telemetryService = $q.reject(e);
}
}
return telemetryService;
}
// Build a request object. This takes the request that was
// passed to the capability, and adds source, id, and key
// fields associated with the object (from its type definition
// and/or its model)
function buildRequest(request) {
// Start with any "telemetry" field in type; use that as a
// basis for the request.
var type = domainObject.getCapability("type"),
typeRequest = (type && type.getDefinition().telemetry) || {},
modelTelemetry = domainObject.getModel().telemetry,
fullRequest = Object.create(typeRequest);
// Add properties from the telemetry field of this
// specific domain object.
Object.keys(modelTelemetry).forEach(function (k) {
fullRequest[k] = modelTelemetry[k];
});
// Add properties from this specific requestData call.
Object.keys(request).forEach(function (k) {
fullRequest[k] = request[k];
});
// Ensure an ID and key are present
if (!fullRequest.id) {
fullRequest.id = domainObject.getId();
}
if (!fullRequest.key) {
fullRequest.key = domainObject.getId();
}
return fullRequest;
}
// Issue a request for telemetry data
function requestTelemetry(request) {
// Bring in any defaults from the object model
var fullRequest = buildRequest(request || {}),
source = fullRequest.source,
key = fullRequest.key;
// Pull out the relevant field from the larger,
// structured response.
function getRelevantResponse(response) {
return ((response || {})[source] || {})[key] || {};
}
// Issue a request to the service
function requestTelemetryFromService(telemetryService) {
return telemetryService.requestTelemetry([fullRequest]);
}
// If a telemetryService is not available,
// getTelemetryService() should reject, and this should
// bubble through subsequent then calls.
return getTelemetryService()
.then(requestTelemetryFromService)
.then(getRelevantResponse);
}
return {
/**
* Request telemetry data for this specific domain object.
* @param {TelemetryRequest} [request] parameters for this
* specific request
* @returns {Promise} a promise for the resulting telemetry
* object
*/
requestData: requestTelemetry,
/**
* Get metadata about this domain object's associated
* telemetry.
*/
getMetadata: function () {
// metadata just looks like a request,
// so use buildRequest to bring in both
// type-level and object-level telemetry
// properties
return buildRequest({});
}
};
}
/**
* The telemetry capability is applicable when a
* domain object model has a "telemetry" field.
*/
TelemetryCapability.appliesTo = function (model) {
return (model &&
model.telemetry) ? true : false;
};
return TelemetryCapability;
}
);

View File

@ -0,0 +1,326 @@
/*global define*/
/**
* Module defining TelemetryController. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
"use strict";
/**
* Serves as a reusable controller for views (or parts of views)
* which need to issue requests for telemetry data and use the
* results
*
* @constructor
*/
function TelemetryController($scope, $q, $timeout, $log) {
// Private to maintain in this scope
var self = {
// IDs of domain objects with telemetry
ids: [],
// Containers for latest responses (id->response)
// Used internally; see buildResponseContainer
// for format
response: {},
// Request fields (id->requests)
request: {},
// Number of outstanding requests
pending: 0,
// Array of object metadatas, for easy retrieval
metadatas: [],
// Interval at which to poll for new data
interval: 1000,
// Flag tracking whether or not a request
// is in progress
refreshing: false,
// Used to track whether a new telemetryUpdate
// is being issued.
broadcasting: false
};
// Broadcast that a telemetryUpdate has occurred.
function doBroadcast() {
// This may get called multiple times from
// multiple objects, so set a flag to suppress
// multiple simultaneous events from being
// broadcast, then issue the actual broadcast
// later (via $timeout)
if (!self.broadcasting) {
self.broadcasting = true;
$timeout(function () {
self.broadcasting = false;
$scope.$broadcast("telemetryUpdate");
});
}
}
// Issue a request for new telemetry for one of the
// objects being tracked by this controller
function requestTelemetryForId(id, trackPending) {
var responseObject = self.response[id],
domainObject = responseObject.domainObject,
telemetry = domainObject.getCapability('telemetry');
// Callback for when data comes back
function storeData(data) {
self.pending -= trackPending ? 1 : 0;
responseObject.data = data;
doBroadcast();
}
self.pending += trackPending ? 1 : 0;
// Shouldn't happen, but isn't fatal,
// so warn.
if (!telemetry) {
$log.warn([
"Expected telemetry capability for ",
id,
" but found none. Cannot request data."
].join(""));
// Request won't happen, so don't
// mark it as pending.
self.pending -= trackPending ? 1 : 0;
return;
}
// Issue the request using the object's telemetry capability
return $q.when(telemetry.requestData(self.request))
.then(storeData);
}
// Request telemetry for all objects tracked by this
// controller. A flag is passed to indicate whether the
// pending counter should be incremented (this will
// cause isRequestPending() to change, which we only
// want to happen for requests which have originated
// outside of this controller's polling action.)
function requestTelemetry(trackPending) {
return $q.all(self.ids.map(function (id) {
return requestTelemetryForId(id, trackPending);
}));
}
// Look up domain objects which have telemetry capabilities.
// This will either be the object in view, or object that
// this object delegates its telemetry capability to.
function promiseRelevantDomainObjects(domainObject) {
// If object has been cleared, there are no relevant
// telemetry-providing domain objects.
if (!domainObject) {
return $q.when([]);
}
// Otherwise, try delegation first, and attach the
// object itself if it has a telemetry capability.
return $q.when(domainObject.useCapability(
"delegation",
"telemetry"
)).then(function (result) {
var head = domainObject.hasCapability("telemetry") ?
[ domainObject ] : [],
tail = result || [];
return head.concat(tail);
});
}
// Build the response containers that are used internally
// by this controller to track latest responses, etc, for
// a given domain object.
function buildResponseContainer(domainObject) {
var telemetry = domainObject &&
domainObject.getCapability("telemetry"),
metadata;
if (telemetry) {
metadata = telemetry.getMetadata();
self.response[domainObject.getId()] = {
name: domainObject.getModel().name,
domainObject: domainObject,
metadata: metadata,
pending: 0,
data: {}
};
} else {
// Shouldn't happen, as we've checked for
// telemetry capabilities previously, but
// be defensive.
$log.warn([
"Expected telemetry capability for ",
domainObject.getId(),
" but none was found."
].join(""));
// Create an empty container so subsequent
// behavior won't hit an exception.
self.response[domainObject.getId()] = {
name: domainObject.getModel().name,
domainObject: domainObject,
metadata: {},
pending: 0,
data: {}
};
}
}
// Build response containers (as above) for all
// domain objects, and update some controller-internal
// state to support subsequent calls.
function buildResponseContainers(domainObjects) {
// Build the containers
domainObjects.forEach(buildResponseContainer);
// Maintain a list of relevant ids, to convert
// back from dictionary-like container objects to arrays.
self.ids = domainObjects.map(function (obj) {
return obj.getId();
});
// Keep a reference to all applicable metadata
// to return from getMetadata
self.metadatas = self.ids.map(function (id) {
return self.response[id].metadata;
});
// Issue a request for the new objects, if we
// know what our request looks like
if (self.request) {
requestTelemetry(true);
}
}
// Get relevant telemetry-providing domain objects
// for the domain object which is represented in this
// scope. This will be the domain object itself, or
// its telemetry delegates, or both.
function getTelemetryObjects(domainObject) {
promiseRelevantDomainObjects(domainObject)
.then(buildResponseContainers);
}
// Handle a polling refresh interval
function startTimeout() {
if (!self.refreshing && self.interval !== undefined) {
self.refreshing = true;
$timeout(function () {
if (self.request) {
requestTelemetry(false);
}
self.refreshing = false;
startTimeout();
}, self.interval);
}
}
// Watch for a represented domain object
$scope.$watch("domainObject", getTelemetryObjects);
// Begin polling for data changes
startTimeout();
return {
/**
* Get all telemetry metadata associated with
* telemetry-providing domain objects managed by
* this controller.
*
* This will ordered in the
* same manner as `getTelemetryObjects()` or
* `getResponse()`; that is, the metadata at a
* given index will correspond to the telemetry-providing
* domain object at the same index.
* @returns {Array} an array of metadata objects
*/
getMetadata: function () {
return self.metadatas;
},
/**
* Get all telemetry-providing domain objects managed by
* this controller.
*
* This will ordered in the
* same manner as `getMetadata()` or
* `getResponse()`; that is, the metadata at a
* given index will correspond to the telemetry-providing
* domain object at the same index.
* @returns {DomainObject[]} an array of metadata objects
*/
getTelemetryObjects: function () {
return self.ids.map(function (id) {
return self.response[id].domainObject;
});
},
/**
* Get the latest telemetry response for a specific
* domain object (if an argument is given) or for all
* objects managed by this controller (if no argument
* is supplied.)
*
* In the first form, this returns a single object; in
* the second form, it returns an array ordered in
* same manner as `getMetadata()` or
* `getTelemetryObjects()`; that is, the telemetry
* response at agiven index will correspond to the
* telemetry-providing domain object at the same index.
* @returns {Array} an array of responses
*/
getResponse: function getResponse(arg) {
var id = arg && (typeof arg === 'string' ?
arg : arg.getId());
if (id) {
return (self.response[id] || {}).data;
}
return (self.ids || []).map(getResponse);
},
/**
* Check if the latest request (not counting
* requests from TelemtryController's own polling)
* is still outstanding. Users of the TelemetryController
* may use this method as a condition upon which to
* show user feedback, such as a wait spinner.
*
* @returns {boolean} true if the request is still outstanding
*/
isRequestPending: function () {
return self.pending > 0;
},
/**
* Issue a new data request. This will change the
* request parameters that are passed along to all
* telemetry capabilities managed by this controller.
*/
requestData: function (request) {
self.request = request || {};
return requestTelemetry(true);
},
/**
* Change the interval at which this controller will
* perform its polling activity.
* @param {number} durationMillis the interval at
* which to poll, in milliseconds
*/
setRefreshInterval: function (durationMillis) {
self.interval = durationMillis;
startTimeout();
}
};
}
return TelemetryController;
}
);

View File

@ -0,0 +1,80 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TelemetryAggregator"],
function (TelemetryAggregator) {
"use strict";
describe("The telemetry aggregator", function () {
var mockQ,
mockProviders,
aggregator;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
function mockProvider(key, index) {
var provider = jasmine.createSpyObj(
"provider" + index,
[ "requestTelemetry" ]
);
provider.requestTelemetry.andReturn({ someKey: key });
return provider;
}
beforeEach(function () {
mockQ = jasmine.createSpyObj("$q", [ "all" ]);
mockQ.all.andReturn(mockPromise([]));
mockProviders = [ "a", "b", "c" ].map(mockProvider);
aggregator = new TelemetryAggregator(mockQ, mockProviders);
});
it("passes requests to aggregated providers", function () {
var requests = [
{ someKey: "some value" },
{ someKey: "some other value" }
];
aggregator.requestTelemetry(requests);
mockProviders.forEach(function (mockProvider) {
expect(mockProvider.requestTelemetry)
.toHaveBeenCalledWith(requests);
});
});
it("merges results from all providers", function () {
var capture = jasmine.createSpy("capture");
mockQ.all.andReturn(mockPromise([
{ someKey: "some value" },
{ someOtherKey: "some other value" }
]));
aggregator.requestTelemetry().then(capture);
// Verify that aggregator results were run through
// $q.all
expect(mockQ.all).toHaveBeenCalledWith([
{ someKey: 'a' },
{ someKey: 'b' },
{ someKey: 'c' }
]);
expect(capture).toHaveBeenCalledWith({
someKey: "some value",
someOtherKey: "some other value"
});
});
});
}
);

View File

@ -0,0 +1,129 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TelemetryCapability"],
function (TelemetryCapability) {
"use strict";
describe("The telemetry capability", function () {
var mockInjector,
mockQ,
mockLog,
mockDomainObject,
mockTelemetryService,
mockReject,
telemetry;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockInjector = jasmine.createSpyObj("$injector", ["get"]);
mockQ = jasmine.createSpyObj("$q", ["when", "reject"]);
mockLog = jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getId", "getCapability", "getModel" ]
);
mockTelemetryService = jasmine.createSpyObj(
"telemetryService",
[ "requestTelemetry" ]
);
mockReject = jasmine.createSpyObj("reject", ["then"]);
mockInjector.get.andReturn(mockTelemetryService);
mockQ.when.andCallFake(mockPromise);
mockQ.reject.andReturn(mockReject);
mockDomainObject.getId.andReturn("testId");
mockDomainObject.getModel.andReturn({
telemetry: {
source: "testSource",
key: "testKey"
}
});
// Bubble up...
mockReject.then.andReturn(mockReject);
telemetry = new TelemetryCapability(
mockInjector,
mockQ,
mockLog,
mockDomainObject
);
});
it("applies only to objects with telemetry sources", function () {
expect(TelemetryCapability.appliesTo({
telemetry: { source: "testSource" }
})).toBeTruthy();
expect(TelemetryCapability.appliesTo({
xtelemetry: { source: "testSource" }
})).toBeFalsy();
expect(TelemetryCapability.appliesTo({})).toBeFalsy();
expect(TelemetryCapability.appliesTo()).toBeFalsy();
});
it("gets a telemetry service from the injector", function () {
telemetry.requestData();
expect(mockInjector.get)
.toHaveBeenCalledWith("telemetryService");
});
it("applies request arguments", function () {
telemetry.requestData({ start: 42 });
expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{
id: "testId", // from domain object
source: "testSource", // from model
key: "testKey", // from model
start: 42 // from argument
}]);
});
it("provides telemetry metadata", function () {
expect(telemetry.getMetadata()).toEqual({
id: "testId", // from domain object
source: "testSource",
key: "testKey"
});
});
it("uses domain object as a key if needed", function () {
// Don't include key in telemetry
mockDomainObject.getModel.andReturn({
telemetry: { source: "testSource" }
});
// Should have used the domain object's ID
expect(telemetry.getMetadata()).toEqual({
id: "testId", // from domain object
source: "testSource", // from model
key: "testId" // from domain object
});
});
it("warns if no telemetry service can be injected", function () {
mockInjector.get.andCallFake(function () { throw ""; });
// Verify precondition
expect(mockLog.warn).not.toHaveBeenCalled();
telemetry.requestData();
expect(mockLog.warn).toHaveBeenCalled();
});
});
}
);

View File

@ -0,0 +1,193 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TelemetryController"],
function (TelemetryController) {
"use strict";
describe("The telemetry controller", function () {
var mockScope,
mockQ,
mockTimeout,
mockLog,
mockDomainObject,
mockTelemetry,
controller;
function mockPromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$on", "$broadcast", "$watch" ]
);
mockQ = jasmine.createSpyObj("$q", [ "all", "when" ]);
mockTimeout = jasmine.createSpy("$timeout");
mockLog = jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getCapability",
"getModel",
"hasCapability",
"useCapability"
]
);
mockTelemetry = jasmine.createSpyObj(
"telemetry",
[ "requestData", "getMetadata" ]
);
mockQ.when.andCallFake(mockPromise);
mockQ.all.andReturn(mockPromise([mockDomainObject]));
mockDomainObject.getId.andReturn("testId");
mockDomainObject.getModel.andReturn({ name: "TEST" });
mockDomainObject.useCapability.andReturn([]);
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockTelemetry);
mockTelemetry.getMetadata.andReturn({
source: "testSource",
key: "testKey"
});
mockTelemetry.requestData.andReturn(mockPromise({
telemetryKey: "some value"
}));
controller = new TelemetryController(
mockScope,
mockQ,
mockTimeout,
mockLog
);
});
it("watches the domain object in scope", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
});
it("starts a refresh interval", function () {
expect(mockTimeout).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Number)
);
});
it("changes refresh interval on request", function () {
controller.setRefreshInterval(42);
// Tick the clock; should issue a new request, with
// the new interval
mockTimeout.mostRecentCall.args[0]();
expect(mockTimeout).toHaveBeenCalledWith(
jasmine.any(Function),
42
);
});
it("requests data from domain objects", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTelemetry.requestData).toHaveBeenCalled();
});
it("logs a warning if no telemetry capability exists", function () {
mockDomainObject.getCapability.andReturn(undefined);
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockLog.warn).toHaveBeenCalled();
});
it("provides telemetry metadata", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.getMetadata()).toEqual([
{ source: "testSource", key: "testKey" }
]);
});
it("provides telemetry-possessing domain objects", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.getTelemetryObjects())
.toEqual([mockDomainObject]);
});
it("provides telemetry data", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.getResponse())
.toEqual([{telemetryKey: "some value"}]);
});
it("provides telemetry data per-id", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.getResponse("testId"))
.toEqual({telemetryKey: "some value"});
});
it("provides a check for pending requests", function () {
expect(controller.isRequestPending()).toBeFalsy();
});
it("allows a request to be specified", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
controller.requestData({ someKey: "some request" });
expect(mockTelemetry.requestData).toHaveBeenCalledWith({
someKey: "some request"
});
});
it("allows an object to be removed from scope", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](undefined);
expect(controller.getTelemetryObjects())
.toEqual([]);
});
it("broadcasts when telemetry is available", function () {
// Push into the scope...
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
controller.requestData({ someKey: "some request" });
// Verify precondition
expect(mockScope.$broadcast).not.toHaveBeenCalled();
// Call the broadcast timeout
mockTimeout.mostRecentCall.args[0]();
// Should have broadcast a telemetryUpdate
expect(mockScope.$broadcast)
.toHaveBeenCalledWith("telemetryUpdate");
});
});
}
);

View File

@ -0,0 +1,5 @@
[
"TelemetryAggregator",
"TelemetryCapability",
"TelemetryController"
]