From 6d5530ba9cd87351795b6b97b48efb9e6b927e32 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Dec 2016 12:08:52 -0800 Subject: [PATCH 01/12] [Tables] Using new composition API to fetch all telemetry objects --- .../controllers/TelemetryTableController.js | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 7d6cbc2bec..d845c13d2f 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -26,9 +26,11 @@ */ define( [ - '../TableConfiguration' + '../TableConfiguration', + '../../../../../src/api/objects/object-utils' + ], - function (TableConfiguration) { + function (TableConfiguration, objectUtils) { /** * The TableController is responsible for getting data onto the page @@ -56,6 +58,8 @@ define( telemetryFormatter); this.changeListeners = []; this.conductor = openmct.conductor; + this.openmct = openmct; + this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), $scope.domainObject.getId()); $scope.rows = []; @@ -156,16 +160,47 @@ define( only). */ TelemetryTableController.prototype.subscribe = function () { + var telemetryApi = this.openmct.telemetry; + if (this.handle) { this.handle.unsubscribe(); } this.$scope.loading = true; + function map(func){ + return function (objects) { + return Promise.all(objects.map(func)); + } + } + + function add(object){ + return function (objects) { + objects.unshift(object); + return objects; + } + } + + function subscribeTo(object) { + return telemetryApi.request(object, {}); + } + + function error() { + console.log("Unable to subscribe"); + } + + this.openmct.composition.get(this.newObject) + .load() + .then(add(this.newObject)) + .then(map(subscribeTo)) + .then(function (telemetry) { + console.log(telemetry.length); + }).catch(error); + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - this.addRealtimeData.bind(this), - true // Lossless - ); + this.$scope.domainObject, + this.addRealtimeData.bind(this), + true // Lossless + ); this.handle.request({}).then(this.addHistoricalData.bind(this)); From 976333d7f740be8d6eb5a43c1c98dc2c15757dd8 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Dec 2016 18:04:47 -0800 Subject: [PATCH 02/12] [Tables] Support for subscriptions from new Telemetry API Historical and real-time data flowing Added formatting, and limits. Support telemetry objects themselves and not just composition of telemetry objects Apply default time range if none supplied (15 minutes) --- example/generator/bundle.js | 9 +- example/generator/src/generatorWorker.js | 8 +- platform/features/table/bundle.js | 68 +----- .../table/res/templates/rt-table.html | 12 - ...orical-table.html => telemetry-table.html} | 2 +- platform/features/table/src/DomainColumn.js | 62 ------ platform/features/table/src/NameColumn.js | 52 ----- platform/features/table/src/RangeColumn.js | 65 ------ .../features/table/src/TableConfiguration.js | 50 +++-- .../controllers/HistoricalTableController.js | 141 ------------ .../controllers/RealtimeTableController.js | 76 ------- .../controllers/TelemetryTableController.js | 206 +++++++++--------- 12 files changed, 159 insertions(+), 592 deletions(-) delete mode 100644 platform/features/table/res/templates/rt-table.html rename platform/features/table/res/templates/{historical-table.html => telemetry-table.html} (82%) delete mode 100644 platform/features/table/src/DomainColumn.js delete mode 100644 platform/features/table/src/NameColumn.js delete mode 100644 platform/features/table/src/RangeColumn.js delete mode 100644 platform/features/table/src/controllers/HistoricalTableController.js delete mode 100644 platform/features/table/src/controllers/RealtimeTableController.js diff --git a/example/generator/bundle.js b/example/generator/bundle.js index 259c5cff15..f1c0f83224 100644 --- a/example/generator/bundle.js +++ b/example/generator/bundle.js @@ -75,8 +75,7 @@ define([ }, { "key": "delta", - "name": "Delta", - "format": "example.delta" + "name": "Delta" } ], "priority": -1 @@ -103,11 +102,13 @@ define([ "domains": [ { "key": "utc", - "name": "Time" + "name": "Time", + "format": "utc" }, { "key": "yesterday", - "name": "Yesterday" + "name": "Yesterday", + "format": "utc" }, { "key": "delta", diff --git a/example/generator/src/generatorWorker.js b/example/generator/src/generatorWorker.js index bb4e55ca4b..091297e185 100644 --- a/example/generator/src/generatorWorker.js +++ b/example/generator/src/generatorWorker.js @@ -24,6 +24,7 @@ (function () { + var FIFTEEN_MINUTES = 15 * 60 * 1000; var handlers = { subscribe: onSubscribe, @@ -82,8 +83,11 @@ function onRequest(message) { var data = message.data; - if (!data.start || !data.end) { - throw new Error('missing start and end!'); + if (data.end == undefined) { + data.end = Date.now(); + } + if (data.start == undefined){ + data.start = data.end - FIFTEEN_MINUTES; } var now = Date.now(); diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 02b78f847f..c034677a02 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -22,25 +22,21 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RealtimeTableController", - "./src/controllers/HistoricalTableController", + "./src/controllers/TelemetryTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "text!./res/templates/table-options-edit.html", - "text!./res/templates/rt-table.html", - "text!./res/templates/historical-table.html", + "text!./res/templates/telemetry-table.html", "legacyRegistry" ], function ( MCTTable, - RealtimeTableController, - HistoricalTableController, + TelemetryTableController, TableOptionsController, Region, InspectorRegion, tableOptionsEditTemplate, - rtTableTemplate, - historicalTableTemplate, + telemetryTableTemplate, legacyRegistry ) { /** @@ -65,9 +61,9 @@ define([ "types": [ { "key": "table", - "name": "Historical Telemetry Table", - "cssclass": "icon-tabular", - "description": "A static table of all values over time for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. The number of rows is based on the range of your query. New incoming data must be manually re-queried for.", + "name": "Telemetry Table", + "cssclass": "icon-tabular-realtime", + "description": "A table of values over a given time period. The table will be automatically updated with new values as they become available", "priority": 861, "features": "creation", "delegates": [ @@ -85,42 +81,13 @@ define([ "views": [ "table" ] - }, - { - "key": "rttable", - "name": "Real-time Telemetry Table", - "cssclass": "icon-tabular-realtime", - "description": "A scrolling table of latest values for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. New incoming data is automatically added to the view.", - "priority": 860, - "features": "creation", - "delegates": [ - "telemetry" - ], - "inspector": tableInspector, - "contains": [ - { - "has": "telemetry" - } - ], - "model": { - "composition": [] - }, - "views": [ - "rt-table", - "scrolling-table" - ] } ], "controllers": [ { - "key": "HistoricalTableController", - "implementation": HistoricalTableController, - "depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout", "openmct"] - }, - { - "key": "RealtimeTableController", - "implementation": RealtimeTableController, - "depends": ["$scope", "telemetryHandler", "telemetryFormatter", "openmct"] + "key": "TelemetryTableController", + "implementation": TelemetryTableController, + "depends": ["$scope", "openmct"] }, { "key": "TableOptionsController", @@ -131,21 +98,10 @@ define([ ], "views": [ { - "name": "Historical Table", + "name": "Telemetry Table", "key": "table", - "template": historicalTableTemplate, - "cssclass": "icon-tabular", - "needs": [ - "telemetry" - ], - "delegation": true, - "editable": false - }, - { - "name": "Real-time Table", - "key": "rt-table", "cssclass": "icon-tabular-realtime", - "template": rtTableTemplate, + "template": telemetryTableTemplate, "needs": [ "telemetry" ], diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html deleted file mode 100644 index da08b0ee8e..0000000000 --- a/platform/features/table/res/templates/rt-table.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/platform/features/table/res/templates/historical-table.html b/platform/features/table/res/templates/telemetry-table.html similarity index 82% rename from platform/features/table/res/templates/historical-table.html rename to platform/features/table/res/templates/telemetry-table.html index c2abbf5708..6dae139263 100644 --- a/platform/features/table/res/templates/historical-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -1,4 +1,4 @@ -
0) { - self.addColumn(new NameColumn(), 0); - } } return this; }; @@ -99,9 +102,8 @@ define( * @returns {Array} The titles of the columns */ TableConfiguration.prototype.getHeaders = function () { - var self = this; return this.columns.map(function (column, i) { - return self.getColumnTitle(column) || 'Column ' + (i + 1); + return column.getTitle()|| 'Column ' + (i + 1); }); }; @@ -113,11 +115,11 @@ define( * @returns {Object} Key value pairs where the key is the column * title, and the value is the formatted value from the provided datum. */ - TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) { + TableConfiguration.prototype.getRowValues = function (limitEvaluator, datum) { var self = this; return this.columns.reduce(function (rowObject, column, i) { var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1), - columnValue = column.getValue(telemetryObject, datum); + columnValue = column.getValue(datum, limitEvaluator); if (columnValue !== undefined && columnValue.text === undefined) { columnValue.text = ''; diff --git a/platform/features/table/src/controllers/HistoricalTableController.js b/platform/features/table/src/controllers/HistoricalTableController.js deleted file mode 100644 index 0f56f6b4ee..0000000000 --- a/platform/features/table/src/controllers/HistoricalTableController.js +++ /dev/null @@ -1,141 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -define( - [ - './TelemetryTableController' - ], - function (TableController) { - var BATCH_SIZE = 1000; - - /** - * Extends TelemetryTableController and adds real-time streaming - * support. - * @memberof platform/features/table - * @param $scope - * @param telemetryHandler - * @param telemetryFormatter - * @constructor - */ - function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout, openmct) { - var self = this; - - this.$timeout = $timeout; - this.timeoutHandle = undefined; - this.batchSize = BATCH_SIZE; - - $scope.$on("$destroy", function () { - if (self.timeoutHandle) { - self.$timeout.cancel(self.timeoutHandle); - } - }); - - TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct); - } - - HistoricalTableController.prototype = Object.create(TableController.prototype); - - /** - * Set provided row data on scope, and cancel loading spinner - * @private - */ - HistoricalTableController.prototype.doneProcessing = function (rowData) { - this.$scope.rows = rowData; - this.$scope.loading = false; - }; - - /** - * @private - */ - HistoricalTableController.prototype.registerChangeListeners = function () { - TableController.prototype.registerChangeListeners.call(this); - //Change of bounds in time conductor - this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', - this.boundsChange.bind(this)) - ); - }; - - /** - * @private - */ - HistoricalTableController.prototype.boundsChange = function (event, bounds, follow) { - // If in follow mode, don't bother re-subscribing, data will be - // received from existing subscription. - if (follow !== true) { - this.subscribe(); - } - }; - - /** - * Processes an array of objects, formatting the telemetry available - * for them and setting it on scope when done - * @private - */ - HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) { - var telemetryObject = objects[offset], - series, - i = start, - pointCount, - end; - - //No more objects to process - if (!telemetryObject) { - return this.doneProcessing(rowData); - } - - series = this.handle.getSeries(telemetryObject); - - pointCount = series.getPointCount(); - end = Math.min(start + this.batchSize, pointCount); - - //Process rows in a batch with size not exceeding a maximum length - for (; i < end; i++) { - rowData.push(this.table.getRowValues(telemetryObject, - this.handle.makeDatum(telemetryObject, series, i))); - } - - //Done processing all rows for this object. - if (end >= pointCount) { - offset++; - end = 0; - } - - // Done processing either a batch or an object, yield process - // before continuing processing - this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData)); - }; - - /** - * Populates historical data on scope when it becomes available from - * the telemetry API - */ - HistoricalTableController.prototype.addHistoricalData = function () { - if (this.timeoutHandle) { - this.$timeout.cancel(this.timeoutHandle); - } - - this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, [])); - }; - - return HistoricalTableController; - } -); diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js deleted file mode 100644 index c6ff7b8aee..0000000000 --- a/platform/features/table/src/controllers/RealtimeTableController.js +++ /dev/null @@ -1,76 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -define( - [ - './TelemetryTableController' - ], - function (TableController) { - - /** - * Extends TelemetryTableController and adds real-time streaming - * support. - * @memberof platform/features/table - * @param $scope - * @param telemetryHandler - * @param telemetryFormatter - * @constructor - */ - function RealtimeTableController($scope, telemetryHandler, telemetryFormatter, openmct) { - TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct); - - this.maxRows = 100000; - } - - RealtimeTableController.prototype = Object.create(TableController.prototype); - - /** - * Overrides method on TelemetryTableController providing handling - * for realtime data. - */ - RealtimeTableController.prototype.addRealtimeData = function () { - var self = this, - datum, - row; - this.handle.getTelemetryObjects().forEach(function (telemetryObject) { - datum = self.handle.getDatum(telemetryObject); - if (datum) { - //Populate row values from telemetry datum - row = self.table.getRowValues(telemetryObject, datum); - self.$scope.rows.push(row); - - //Inform table that a new row has been added - if (self.$scope.rows.length > self.maxRows) { - self.$scope.$broadcast('remove:row', 0); - self.$scope.rows.shift(); - } - - self.$scope.$broadcast('add:row', - self.$scope.rows.length - 1); - } - }); - this.$scope.loading = false; - }; - - return RealtimeTableController; - } -); diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index d845c13d2f..8eea6887dc 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -38,14 +38,10 @@ define( * configuration, and telemetry subscriptions. * @memberof platform/features/table * @param $scope - * @param telemetryHandler - * @param telemetryFormatter * @constructor */ function TelemetryTableController( $scope, - telemetryHandler, - telemetryFormatter, openmct ) { var self = this; @@ -53,9 +49,8 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; - this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, - telemetryFormatter); + openmct); this.changeListeners = []; this.conductor = openmct.conductor; this.openmct = openmct; @@ -68,6 +63,9 @@ define( self.subscribe(); self.registerChangeListeners(); }); + this.mutationListener = openmct.objects.observe(this.newObject, "*", function (domainObject){ + self.newObject = domainObject; + }); this.destroy = this.destroy.bind(this); @@ -79,6 +77,8 @@ define( this.sortByTimeSystem = this.sortByTimeSystem.bind(this); this.conductor.on('timeSystem', this.sortByTimeSystem); this.conductor.off('timeSystem', this.sortByTimeSystem); + + this.subscriptions = []; } /** @@ -130,29 +130,12 @@ define( * Release the current subscription (called when scope is destroyed) */ TelemetryTableController.prototype.destroy = function () { - if (this.handle) { - this.handle.unsubscribe(); - this.handle = undefined; - } + this.subscriptions.forEach(function (subscription) { + subscription() + }); + this.mutationListener(); }; - /** - * Function for handling realtime data when it is available. This - * will be called by the telemetry framework when new data is - * available. - * - * Method should be overridden by specializing class. - */ - TelemetryTableController.prototype.addRealtimeData = function () { - }; - - /** - * Function for handling historical data. Will be called by - * telemetry framework when requested historical data is available. - * Should be overridden by specializing class. - */ - TelemetryTableController.prototype.addHistoricalData = function () { - }; /** Create a new subscription. This can be overridden by children to @@ -160,94 +143,123 @@ define( only). */ TelemetryTableController.prototype.subscribe = function () { + var self = this; var telemetryApi = this.openmct.telemetry; + var compositionApi = this.openmct.composition; + var subscriptions = this.subscriptions; + var tableConfiguration = this.table; + var scope = this.$scope; + var maxRows = 100000; + var conductor = this.conductor; + var newObject = this.newObject; - if (this.handle) { - this.handle.unsubscribe(); - } this.$scope.loading = true; - function map(func){ - return function (objects) { - return Promise.all(objects.map(func)); + function makeTableRows(object, historicalData){ + var limitEvaluator = telemetryApi.limitEvaluator(object); + return historicalData.map(tableConfiguration.getRowValues.bind(tableConfiguration, limitEvaluator)); + } + + function requestData(objects) { + var bounds = conductor.bounds(); + + return Promise.all( + objects.map(function (object) { + return telemetryApi.request(object, { + start: bounds.start, + end: bounds.end + }).then( + makeTableRows.bind(this, object) + ); + }) + ); + } + + function addHistoricalData(historicalData){ + scope.rows = Array.prototype.concat.apply([], historicalData); + scope.loading = false; + } + + function newData(domainObject, datum) { + scope.rows.push(tableConfiguration.getRowValues(datum, telemetryApi.limitEvaluator(domainObject))); + + //Inform table that a new row has been added + if (scope.rows.length > maxRows) { + scope.$broadcast('remove:row', 0); + scope.rows.shift(); } + + scope.$broadcast('add:row', + scope.rows.length - 1); + } - function add(object){ - return function (objects) { - objects.unshift(object); - return objects; - } + function subscribe(objects) { + objects.forEach(function (object){ + subscriptions.push(telemetryApi.subscribe(object, newData.bind(this, object), {})); + }); + return objects; } - function subscribeTo(object) { - return telemetryApi.request(object, {}); + function error(e) { + throw e; } - function error() { - console.log("Unable to subscribe"); + function loadColumns(objects) { + var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); + var allColumns = telemetryApi.commonValuesForHints(metadatas, []); + + tableConfiguration.populateColumns(allColumns); + + this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum){ + return metadatum.name; + }); + + self.filterColumns(); + + return Promise.resolve(objects); } - this.openmct.composition.get(this.newObject) - .load() - .then(add(this.newObject)) - .then(map(subscribeTo)) - .then(function (telemetry) { - console.log(telemetry.length); - }).catch(error); - - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - this.addRealtimeData.bind(this), - true // Lossless - ); - - this.handle.request({}).then(this.addHistoricalData.bind(this)); - - this.setup(); - }; - - TelemetryTableController.prototype.populateColumns = function (telemetryMetadata) { - this.table.populateColumns(telemetryMetadata); - - //Identify time columns - telemetryMetadata.forEach(function (metadatum) { - //Push domains first - (metadatum.domains || []).forEach(function (domainMetadata) { - this.timeColumns.push(domainMetadata.name); - }.bind(this)); - }.bind(this)); - - var timeSystem = this.conductor.timeSystem(); - if (timeSystem) { - this.sortByTimeSystem(timeSystem); + function filterForTelemetry(objects){ + return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi)); } - }; - /** - * Setup table columns based on domain object metadata - */ - TelemetryTableController.prototype.setup = function () { - var handle = this.handle, - self = this; + function getDomainObjects() { + return new Promise(function (resolve, reject){ + var objects = [newObject]; + var composition = compositionApi.get(newObject); - if (handle) { - this.timeColumns = []; - handle.promiseTelemetryObjects().then(function () { - self.$scope.headers = []; - self.$scope.rows = []; - - self.populateColumns(handle.getMetadata()); - self.filterColumns(); - - // When table column configuration changes, (due to being - // selected or deselected), filter columns appropriately. - self.changeListeners.push(self.$scope.$watchCollection( - 'domainObject.getModel().configuration.table.columns', - self.filterColumns.bind(self) - )); + if (composition) { + composition + .load() + .then(function (children) { + return objects.concat(children); + }) + .then(resolve) + .catch(reject); + } else { + return resolve(objects); + } }); } + + scope.headers = []; + scope.rows = []; + + getDomainObjects() + .then(filterForTelemetry) + .catch(error) + .then(function (objects){ + if (objects.length > 0){ + return loadColumns(objects) + .then(subscribe) + .then(requestData) + .then(addHistoricalData) + .catch(error); + } else { + scope.loading = false; + } + }) }; /** From 3544caf4be5ae933548993c30c53813a24d706cd Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 15 Dec 2016 15:21:45 -0800 Subject: [PATCH 03/12] [API] Observer path was accessing object key incorrectly --- src/api/objects/MutableObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js index 1eb5fe4e0e..e4a1d477c9 100644 --- a/src/api/objects/MutableObject.js +++ b/src/api/objects/MutableObject.js @@ -41,7 +41,7 @@ define([ } function qualifiedEventName(object, eventName) { - return [object.key.identifier, eventName].join(':'); + return [object.identifier.key, eventName].join(':'); } MutableObject.prototype.stopListening = function () { From 2a4944d6ee44b732c98083075945b6de53bb2c25 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 16 Dec 2016 16:34:41 -0800 Subject: [PATCH 04/12] [Tables] Refactoring for consolidation of historical and real-time tables Added batch processing of large historical queries. #1077 --- platform/features/plot/src/PlotController.js | 2 +- platform/features/table/bundle.js | 2 +- .../table/res/templates/mct-table.html | 8 +- .../table/res/templates/telemetry-table.html | 2 +- .../features/table/src/TableConfiguration.js | 1 + .../src/controllers/MCTTableController.js | 118 ++++-- .../controllers/TelemetryTableController.js | 379 ++++++++++++------ .../features/table/src/directives/MCTTable.js | 6 +- src/api/telemetry/TelemetryValueFormatter.js | 24 +- 9 files changed, 365 insertions(+), 177 deletions(-) diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index e4be264ea9..ea34e1b878 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -217,8 +217,8 @@ define( if (handle) { handle.unsubscribe(); handle = undefined; - conductor.off("timeOfInterest", changeTimeOfInterest); } + conductor.off("timeOfInterest", changeTimeOfInterest); } function requery() { diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index c034677a02..b5d67d626b 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -87,7 +87,7 @@ define([ { "key": "TelemetryTableController", "implementation": TelemetryTableController, - "depends": ["$scope", "openmct"] + "depends": ["$scope", "$timeout", "openmct"] }, { "key": "TableOptionsController", diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 7e24be2c43..3a805bf4e0 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -5,7 +5,7 @@ Export
-
+
@@ -32,8 +32,7 @@ enableSort ? 'sortable' : '', sortColumn === header ? 'sort' : '', sortDirection || '' - ].join(' ')" - ng-click="toggleSort(header)"> + ].join(' ')"> {{ header }} @@ -59,8 +58,7 @@ + ng-style="{ top: visibleRow.offsetY + 'px' }">
\ No newline at end of file diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index f6d33d3269..a63ea569d6 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -53,6 +53,7 @@ define( var formatter = telemetryApi.getValueFormatter(metadatum); self.addColumn({ + metadata: metadatum, getTitle: function () { return metadatum.name; }, diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 3ab1887c29..e9c18400ee 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -12,12 +12,12 @@ define( * @param element * @constructor */ - function MCTTableController($scope, $timeout, element, exportService, formatService, openmct) { + function MCTTableController($scope, $window, element, exportService, formatService, openmct) { var self = this; this.$scope = $scope; this.element = $(element[0]); - this.$timeout = $timeout; + this.$window = $window; this.maxDisplayRows = 50; this.scrollable = this.element.find('.l-view-section.scrolling').first(); @@ -27,15 +27,39 @@ define( this.conductor = openmct.conductor; this.toiFormatter = undefined; this.formatService = formatService; + this.callbacks = {}; //Bind all class functions to 'this' - Object.keys(MCTTableController.prototype).filter(function (key) { - return typeof MCTTableController.prototype[key] === 'function'; - }).forEach(function (key) { - this[key] = MCTTableController.prototype[key].bind(this); - }.bind(this)); + _.bindAll(this, [ + 'destroyConductorListeners', + 'changeTimeSystem', + 'scrollToBottom', + 'addRow', + 'removeRow', + 'onScroll', + 'firstVisible', + 'lastVisible', + 'setVisibleRows', + 'setHeaders', + 'setElementSizes', + 'binarySearch', + 'insertSorted', + 'sortComparator', + 'sortRows', + 'buildLargestRow', + 'resize', + 'filterAndSort', + 'setRows', + 'filterRows', + 'scrollToRow', + 'setTimeOfInterestRow', + 'changeTimeOfInterest', + 'changeBounds', + 'onRowClick', + 'digest' + ]); - this.scrollable.on('scroll', this.onScroll.bind(this)); + this.scrollable.on('scroll', this.onScroll); $scope.visibleRows = []; @@ -86,7 +110,7 @@ define( $scope.sortDirection = 'asc'; } self.setRows($scope.rows); - self.setTimeOfInterest(self.conductor.timeOfInterest()); + self.setTimeOfInterestRow(self.conductor.timeOfInterest()); }; /* @@ -108,7 +132,11 @@ define( * Populated from the default-sort attribute on MctTable * directive tag. */ - $scope.$watch('sortColumn', $scope.toggleSort); + $scope.$watch('defaultSort', function (newColumn, oldColumn) { + if (newColumn !== oldColumn) { + $scope.toggleSort(newColumn) + } + }); /* * Listen for resize events to trigger recalculation of table width @@ -125,7 +153,7 @@ define( this.destroyConductorListeners(); this.conductor.on('timeSystem', this.changeTimeSystem); - this.conductor.on('timeOfInterest', this.setTimeOfInterest); + this.conductor.on('timeOfInterest', this.changeTimeOfInterest); this.conductor.on('bounds', this.changeBounds); // If time system defined, set initially @@ -135,12 +163,22 @@ define( } }.bind(this)); - $scope.$on('$destroy', this.destroyConductorListeners); - } + console.log('constructed'); + + $scope.$on('$destroy', function() { + this.scrollable.off('scroll', this.onScroll); + this.destroyConductorListeners(); + + // In case for some reason this controller instance lingers around, + // destroy scope as it can be extremely large for large tables. + delete this.$scope; + + }.bind(this)); + }; MCTTableController.prototype.destroyConductorListeners = function () { this.conductor.off('timeSystem', this.changeTimeSystem); - this.conductor.off('timeOfInterest', this.setTimeOfInterest); + this.conductor.off('timeOfInterest', this.changeTimeOfInterest); this.conductor.off('bounds', this.changeBounds); }; @@ -160,7 +198,7 @@ define( //Use timeout to defer execution until next digest when any // pending UI changes have completed, eg. a new row in the table. if (this.$scope.autoScroll) { - this.$timeout(function () { + this.digest(function () { self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight; }); } @@ -183,6 +221,12 @@ define( this.resize([this.$scope.sizingRow, row]) .then(this.setVisibleRows.bind(this)) .then(this.scrollToBottom.bind(this)); + + var toi = this.conductor.timeOfInterest(); + if (toi !== -1) { + this.setTimeOfInterestRow(toi); + } + } }; @@ -193,8 +237,8 @@ define( */ MCTTableController.prototype.removeRow = function (event, rowIndex) { var row = this.$scope.rows[rowIndex], - // Do a sequential search here. Only way of finding row is by - // object equality, so array is in effect unsorted. + // Do a sequential search here. Only way of finding row is by + // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); if (indexInDisplayRows !== -1) { this.$scope.displayRows.splice(indexInDisplayRows, 1); @@ -522,6 +566,27 @@ define( return largestRow; }; + MCTTableController.prototype.digest = function (callback) { + var scope = this.$scope; + var callbacks = this.callbacks; + var requestAnimationFrame = this.$window.requestAnimationFrame; + + var promise = callbacks[callback]; + + if (!promise){ + promise = new Promise(function (resolve) { + requestAnimationFrame(function() { + scope.$digest(); + delete callbacks[callback]; + resolve(callback && callback()); + }); + }); + callbacks[callback] = promise; + } + + return promise; + }; + /** * Calculates the widest row in the table, and if necessary, resizes * the table accordingly @@ -533,7 +598,7 @@ define( */ MCTTableController.prototype.resize = function (rows) { this.$scope.sizingRow = this.buildLargestRow(rows); - return this.$timeout(this.setElementSizes.bind(this)); + return this.digest(this.setElementSizes); }; /** @@ -566,15 +631,15 @@ define( .then(this.setVisibleRows) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. - .then(this.$timeout) + .then(this.digest) .then(function () { //If TOI specified, scroll to it var timeOfInterest = this.conductor.timeOfInterest(); if (timeOfInterest) { - this.setTimeOfInterest(timeOfInterest); + this.setTimeOfInterestRow(timeOfInterest); + this.scrollToRow(this.$scope.toiRowIndex); } }.bind(this)); - }; /** @@ -635,7 +700,7 @@ define( * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.setTimeOfInterest = function (newTOI) { + MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) { var isSortedByTime = this.$scope.timeColumns && this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1; @@ -652,17 +717,22 @@ define( if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) { this.$scope.toiRowIndex = rowIndex; - this.scrollToRow(this.$scope.toiRowIndex); } } }; + MCTTableController.prototype.changeTimeOfInterest = function (newTOI) { + this.setTimeOfInterestRow(newTOI); + this.scrollToRow(this.$scope.toiRowIndex); + }; + /** * On zoom, pan, etc. reset TOI * @param bounds */ MCTTableController.prototype.changeBounds = function (bounds) { - this.setTimeOfInterest(this.conductor.timeOfInterest()); + this.setTimeOfInterestRow(this.conductor.timeOfInterest()); + this.scrollToRow(this.$scope.toiRowIndex); }; /** diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 8eea6887dc..5aa89e8f1e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -42,43 +42,47 @@ define( */ function TelemetryTableController( $scope, + $timeout, openmct ) { - var self = this; - this.$scope = $scope; + this.$timeout = $timeout; + this.openmct = openmct; + this.batchSize = 1000; + + /* + * Initialization block + */ this.columns = {}; //Range and Domain columns - this.handle = undefined; + this.deregisterListeners = []; + this.subscriptions = []; + this.timeColumns = []; + $scope.rows = []; this.table = new TableConfiguration($scope.domainObject, openmct); - this.changeListeners = []; - this.conductor = openmct.conductor; - this.openmct = openmct; - this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), $scope.domainObject.getId()); + this.lastBounds = this.openmct.conductor.bounds(); + this.requestTime = 0; - $scope.rows = []; + /* + * Create a new format object from legacy object, and replace it + * when it changes + */ + this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), + $scope.domainObject.getId()); - // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function () { - self.subscribe(); - self.registerChangeListeners(); - }); - this.mutationListener = openmct.objects.observe(this.newObject, "*", function (domainObject){ - self.newObject = domainObject; - }); + _.bindAll(this, [ + 'destroy', + 'sortByTimeSystem', + 'loadColumns', + 'getHistoricalData', + 'subscribeToNewData', + 'changeBounds' + ]); - this.destroy = this.destroy.bind(this); + this.getData(); + this.registerChangeListeners(); - // Unsubscribe when the plot is destroyed this.$scope.$on("$destroy", this.destroy); - this.timeColumns = []; - - - this.sortByTimeSystem = this.sortByTimeSystem.bind(this); - this.conductor.on('timeSystem', this.sortByTimeSystem); - this.conductor.off('timeSystem', this.sortByTimeSystem); - - this.subscriptions = []; } /** @@ -91,133 +95,254 @@ define( scope.defaultSort = undefined; if (timeSystem) { this.table.columns.forEach(function (column) { - if (column.domainMetadata && column.domainMetadata.key === timeSystem.metadata.key) { + if (column.metadata.key === timeSystem.metadata.key) { scope.defaultSort = column.getTitle(); } }); + this.$scope.rows = _.sortBy(this.$scope.rows, function (row) { + return row[this.$scope.defaultSort]; + }); } }; - TelemetryTableController.prototype.unregisterChangeListeners = function () { - this.changeListeners.forEach(function (listener) { - return listener && listener(); - }); - this.changeListeners = []; - }; - /** - * Defer registration of change listeners until domain object is - * available in order to avoid race conditions + * Attach listeners to domain object to respond to changes due to + * composition, etc. * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - var self = this; - this.unregisterChangeListeners(); + this.deregisterListeners.forEach(function (deregister){ + deregister(); + }); + this.deregisterListeners = []; - // When composition changes, re-subscribe to the various - // telemetry subscriptions - this.changeListeners.push(this.$scope.$watchCollection( - 'domainObject.getModel().composition', - function (newVal, oldVal) { - if (newVal !== oldVal) { - self.subscribe(); - } - }) + this.deregisterListeners.push( + this.openmct.objects.observe(this.newObject, "*", + function (domainObject){ + this.newObject = domainObject; + this.getData(); + }.bind(this) + ) ); + this.openmct.conductor.on('timeSystem', this.sortByTimeSystem); + this.openmct.conductor.on('bounds', this.changeBounds); + }; + + TelemetryTableController.prototype.tick = function (bounds) { + // Can't do ticking until we change how data is handled + // Pass raw values to table, with format function + + /*if (this.$scope.defaultSort) { + this.$scope.rows.filter(function (row){ + return row[] + }) + }*/ + }; + + TelemetryTableController.prototype.changeBounds = function (bounds) { + var follow = this.openmct.conductor.follow(); + var isTick = follow && + bounds.start !== this.lastBounds.start && + bounds.end !== this.lastBounds.end; + var isDeltaChange = follow && + !isTick && + (bounds.start !== this.lastBounds.start || + bounds.end !== this.lastBounds.end); + + if (isTick){ + // Treat it as a realtime tick + // Drop old data that falls outside of bounds + this.tick(bounds); + } else if (isDeltaChange){ + // No idea... + // Historical query for bounds, then tick on + this.getData(); + } else { + // Is fixed bounds change + this.getData(); + } + this.lastBounds = bounds; }; /** * Release the current subscription (called when scope is destroyed) */ TelemetryTableController.prototype.destroy = function () { + + this.openmct.conductor.off('timeSystem', this.sortByTimeSystem); + this.openmct.conductor.off('bounds', this.changeBounds); + this.subscriptions.forEach(function (subscription) { - subscription() + subscription(); }); - this.mutationListener(); + this.deregisterListeners.forEach(function (deregister){ + deregister(); + }); + this.subscriptions = []; + this.deregisterListeners = []; + + if (this.timeoutHandle) { + this.$timeout.cancel(this.timeoutHandle); + } + + // In case controller instance lingers around (currently there is a + // temporary memory leak with PlotController), clean up scope as it + // can be extremely large. + this.$scope = null; + this.table = null; }; + /** + * @private + * @param objects + * @returns {*} + */ + TelemetryTableController.prototype.loadColumns = function (objects) { + var telemetryApi = this.openmct.telemetry; + + if (objects.length > 0) { + var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); + var allColumns = telemetryApi.commonValuesForHints(metadatas, []); + + this.table.populateColumns(allColumns); + + this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { + return metadatum.name; + }); + + this.filterColumns(); + + var timeSystem = this.openmct.conductor.timeSystem(); + if (timeSystem) { + this.sortByTimeSystem(timeSystem); + } + } + return objects; + }; /** - Create a new subscription. This can be overridden by children to - change default behaviour (which is to retrieve historical telemetry - only). + * @private + * @param objects The domain objects to request telemetry for + * @returns {*|{configFile}|app|boolean|Route|Object} */ - TelemetryTableController.prototype.subscribe = function () { - var self = this; + TelemetryTableController.prototype.getHistoricalData = function (objects) { + var openmct = this.openmct; + var bounds = openmct.conductor.bounds(); + var scope = this.$scope; + var processedObjects = 0; + var requestTime = this.lastRequestTime = Date.now(); + + return new Promise(function (resolve, reject){ + console.log('Created promise'); + function finishProcessing(tableRows){ + scope.rows = tableRows; + scope.loading = false; + console.log('Resolved promise'); + resolve(tableRows); + } + + function processData(historicalData, index, rowData, limitEvaluator){ + console.log("Processing batch"); + if (index >= historicalData.length) { + processedObjects++; + + if (processedObjects === objects.length) { + finishProcessing(rowData); + } + } else { + rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) + .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + this.timeoutHandle = this.$timeout(processData.bind( + this, + historicalData, + index + this.batchSize, + rowData, + limitEvaluator + )); + } + } + + function makeTableRows(object, historicalData) { + // Only process one request at a time + if (requestTime === this.lastRequestTime) { + console.log('Processing request'); + var limitEvaluator = openmct.telemetry.limitEvaluator(object); + processData.call(this, historicalData, 0, [], limitEvaluator); + } else { + console.log('Ignoring returned data because of staleness'); + resolve([]); + } + } + + function requestData (object) { + return openmct.telemetry.request(object, { + start: bounds.start, + end: bounds.end + }).then(makeTableRows.bind(this, object)) + .catch(reject); + } + this.$timeout.cancel(this.timeoutHandle); + + if (objects.length > 0){ + objects.forEach(requestData.bind(this)); + } else { + scope.loading = false; + console.log('Resolved promise'); + resolve([]); + } + }.bind(this)); + }; + + /** + * @private + * @param objects + * @returns {*} + */ + TelemetryTableController.prototype.subscribeToNewData = function (objects) { + var telemetryApi = this.openmct.telemetry; + //Set table max length to avoid unbounded growth. + var maxRows = 100000; + + this.subscriptions.forEach(function (subscription) { + subscription(); + }); + this.subscriptions = []; + + function newData(domainObject, datum) { + this.$scope.rows.push(this.table.getRowValues( + telemetryApi.limitEvaluator(domainObject), datum)); + + //Inform table that a new row has been added + if (this.$scope.rows.length > maxRows) { + this.$scope.$broadcast('remove:row', 0); + this.$scope.rows.shift(); + } + + this.$scope.$broadcast('add:row', + this.$scope.rows.length - 1); + + } + + objects.forEach(function (object){ + this.subscriptions.push( + telemetryApi.subscribe(object, newData.bind(this, object), {})); + console.log('subscribed'); + }.bind(this)); + + return objects; + }; + + TelemetryTableController.prototype.getData = function () { var telemetryApi = this.openmct.telemetry; var compositionApi = this.openmct.composition; - var subscriptions = this.subscriptions; - var tableConfiguration = this.table; var scope = this.$scope; - var maxRows = 100000; - var conductor = this.conductor; var newObject = this.newObject; this.$scope.loading = true; - function makeTableRows(object, historicalData){ - var limitEvaluator = telemetryApi.limitEvaluator(object); - return historicalData.map(tableConfiguration.getRowValues.bind(tableConfiguration, limitEvaluator)); - } - - function requestData(objects) { - var bounds = conductor.bounds(); - - return Promise.all( - objects.map(function (object) { - return telemetryApi.request(object, { - start: bounds.start, - end: bounds.end - }).then( - makeTableRows.bind(this, object) - ); - }) - ); - } - - function addHistoricalData(historicalData){ - scope.rows = Array.prototype.concat.apply([], historicalData); - scope.loading = false; - } - - function newData(domainObject, datum) { - scope.rows.push(tableConfiguration.getRowValues(datum, telemetryApi.limitEvaluator(domainObject))); - - //Inform table that a new row has been added - if (scope.rows.length > maxRows) { - scope.$broadcast('remove:row', 0); - scope.rows.shift(); - } - - scope.$broadcast('add:row', - scope.rows.length - 1); - - } - - function subscribe(objects) { - objects.forEach(function (object){ - subscriptions.push(telemetryApi.subscribe(object, newData.bind(this, object), {})); - }); - return objects; - } - function error(e) { - throw e; - } - - function loadColumns(objects) { - var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); - var allColumns = telemetryApi.commonValuesForHints(metadatas, []); - - tableConfiguration.populateColumns(allColumns); - - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum){ - return metadatum.name; - }); - - self.filterColumns(); - - return Promise.resolve(objects); + scope.loading = false; + console.error(e); } function filterForTelemetry(objects){ @@ -248,18 +373,10 @@ define( getDomainObjects() .then(filterForTelemetry) + .then(this.loadColumns) + //.then(this.subscribeToNewData) + .then(this.getHistoricalData) .catch(error) - .then(function (objects){ - if (objects.length > 0){ - return loadColumns(objects) - .then(subscribe) - .then(requestData) - .then(addHistoricalData) - .catch(error); - } else { - scope.loading = false; - } - }) }; /** diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index dad23c2eb5..b240fa7f43 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -77,13 +77,13 @@ define( * * @constructor */ - function MCTTable($timeout) { + function MCTTable() { return { restrict: "E", template: TableTemplate, controller: [ '$scope', - '$timeout', + '$window', '$element', 'exportService', 'formatService', @@ -104,7 +104,7 @@ define( timeColumns: "=?", // Indicate a column to sort on. Allows control of sort // via configuration (eg. for default sort column). - sortColumn: "=?" + defaultSort: "=?" } }; } diff --git a/src/api/telemetry/TelemetryValueFormatter.js b/src/api/telemetry/TelemetryValueFormatter.js index 801aee1176..a5e8cb8720 100644 --- a/src/api/telemetry/TelemetryValueFormatter.js +++ b/src/api/telemetry/TelemetryValueFormatter.js @@ -28,6 +28,18 @@ define([ // TODO: needs reference to formatService; function TelemetryValueFormatter(valueMetadata, formatService) { + var numberFormatter = { + parse: function (x) { + return Number(x); + }, + format: function (x) { + return x; + }, + validate: function (x) { + return true; + } + }; + this.valueMetadata = valueMetadata; this.parseCache = new WeakMap(); this.formatCache = new WeakMap(); @@ -36,17 +48,7 @@ define([ .getFormat(valueMetadata.format, valueMetadata); } catch (e) { // TODO: Better formatting - this.formatter = { - parse: function (x) { - return Number(x); - }, - format: function (x) { - return x; - }, - validate: function (x) { - return true; - } - }; + this.formatter = numberFormatter; } if (valueMetadata.type === 'enum') { From 50f303bbdc020c842731465696df1929b465168a Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 15 Jan 2017 10:59:28 -0800 Subject: [PATCH 05/12] [Tables] limit digests to increase performance --- .../table/res/templates/mct-table.html | 8 +- .../table/res/templates/telemetry-table.html | 5 +- .../src/controllers/MCTTableController.js | 78 ++++++++++--------- .../controllers/TelemetryTableController.js | 44 ++++++----- 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 3a805bf4e0..7e24be2c43 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -5,7 +5,7 @@ Export -
+
@@ -32,7 +32,8 @@ enableSort ? 'sortable' : '', sortColumn === header ? 'sort' : '', sortDirection || '' - ].join(' ')"> + ].join(' ')" + ng-click="toggleSort(header)"> {{ header }} @@ -58,7 +59,8 @@ + ng-style="{ top: visibleRow.offsetY + 'px' }" + ng-click="table.onRowClick($event, visibleRow.rowIndex) "> - @@ -60,9 +60,9 @@ + ng-click="table.onRowClick($event, visibleRow.rowIndex)">
+ Request time: {{tableController.lastRequestTime | date : 'HH:mm:ss:sss'}} diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index e9c18400ee..aef6e68240 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -163,8 +163,6 @@ define( } }.bind(this)); - console.log('constructed'); - $scope.$on('$destroy', function() { this.scrollable.off('scroll', this.onScroll); this.destroyConductorListeners(); @@ -193,15 +191,7 @@ define( * @private */ MCTTableController.prototype.scrollToBottom = function () { - var self = this; - - //Use timeout to defer execution until next digest when any - // pending UI changes have completed, eg. a new row in the table. - if (this.$scope.autoScroll) { - this.digest(function () { - self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight; - }); - } + this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight; }; /** @@ -219,8 +209,12 @@ define( //Resize the columns , then update the rows visible in the table this.resize([this.$scope.sizingRow, row]) - .then(this.setVisibleRows.bind(this)) - .then(this.scrollToBottom.bind(this)); + .then(this.setVisibleRows) + .then(function () { + if (this.$scope.autoScroll) { + this.scrollToBottom(); + } + }.bind(this)); var toi = this.conductor.timeOfInterest(); if (toi !== -1) { @@ -250,16 +244,24 @@ define( * @private */ MCTTableController.prototype.onScroll = function (event) { - //If user scrolls away from bottom, disable auto-scroll. - // Auto-scroll will be re-enabled if user scrolls to bottom again. - if (this.scrollable[0].scrollTop < - (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) { - this.$scope.autoScroll = false; - } else { - this.$scope.autoScroll = true; - } - this.setVisibleRows(); - this.$scope.$digest(); + if (!this.scrolling) { + this.scrolling = true; + + requestAnimationFrame(function () { + this.setVisibleRows(); + this.digest(); + + // If user scrolls away from bottom, disable auto-scroll. + // Auto-scroll will be re-enabled if user scrolls to bottom again. + if (this.scrollable[0].scrollTop < + (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { + this.$scope.autoScroll = false; + } else { + this.$scope.autoScroll = true; + } + this.scrolling = false; + }.bind(this)); + } }; /** @@ -338,7 +340,7 @@ define( this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { - return; // don't update if no changes are required. + return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. @@ -351,6 +353,7 @@ define( contents: row }; }); + return this.digest(); }; /** @@ -566,25 +569,25 @@ define( return largestRow; }; - MCTTableController.prototype.digest = function (callback) { + // Will effectively cap digests at 60Hz + // Also turns digest into a promise allowing code to force digest, then + // schedule something to happen afterwards + MCTTableController.prototype.digest = function () { var scope = this.$scope; - var callbacks = this.callbacks; + var self = this; var requestAnimationFrame = this.$window.requestAnimationFrame; - var promise = callbacks[callback]; - - if (!promise){ - promise = new Promise(function (resolve) { + if (!this.digestPromise) { + this.digestPromise = new Promise(function (resolve) { requestAnimationFrame(function() { scope.$digest(); - delete callbacks[callback]; - resolve(callback && callback()); + delete self.digestPromise; + resolve(); }); }); - callbacks[callback] = promise; } - return promise; + return this.digestPromise; }; /** @@ -598,7 +601,7 @@ define( */ MCTTableController.prototype.resize = function (rows) { this.$scope.sizingRow = this.buildLargestRow(rows); - return this.digest(this.setElementSizes); + return this.digest().then(this.setElementSizes); }; /** @@ -631,7 +634,6 @@ define( .then(this.setVisibleRows) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. - .then(this.digest) .then(function () { //If TOI specified, scroll to it var timeOfInterest = this.conductor.timeOfInterest(); @@ -732,7 +734,9 @@ define( */ MCTTableController.prototype.changeBounds = function (bounds) { this.setTimeOfInterestRow(this.conductor.timeOfInterest()); - this.scrollToRow(this.$scope.toiRowIndex); + if (this.$scope.toiRowIndex !== -1) { + this.scrollToRow(this.$scope.toiRowIndex); + } }; /** diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 5aa89e8f1e..f015d11cab 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -76,15 +76,23 @@ define( 'loadColumns', 'getHistoricalData', 'subscribeToNewData', - 'changeBounds' + 'changeBounds', + 'setScroll' ]); this.getData(); this.registerChangeListeners(); + this.openmct.conductor.on('follow', this.setScroll); + this.setScroll(this.openmct.conductor.follow()); + this.$scope.$on("$destroy", this.destroy); } + TelemetryTableController.prototype.setScroll = function (scroll){ + this.$scope.autoScroll = scroll; + }; + /** * Based on the selected time system, find a matching domain column * to sort by. By default will just match on key. @@ -171,6 +179,7 @@ define( this.openmct.conductor.off('timeSystem', this.sortByTimeSystem); this.openmct.conductor.off('bounds', this.changeBounds); + this.openmct.conductor.off('follow', this.setScroll); this.subscriptions.forEach(function (subscription) { subscription(); @@ -229,20 +238,18 @@ define( var openmct = this.openmct; var bounds = openmct.conductor.bounds(); var scope = this.$scope; + var rowData = []; var processedObjects = 0; var requestTime = this.lastRequestTime = Date.now(); return new Promise(function (resolve, reject){ - console.log('Created promise'); function finishProcessing(tableRows){ - scope.rows = tableRows; + scope.rows = tableRows.concat(scope.rows); scope.loading = false; - console.log('Resolved promise'); resolve(tableRows); } - function processData(historicalData, index, rowData, limitEvaluator){ - console.log("Processing batch"); + function processData(historicalData, index, limitEvaluator){ if (index >= historicalData.length) { processedObjects++; @@ -250,13 +257,14 @@ define( finishProcessing(rowData); } } else { - rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) - .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + rowData = rowData.concat( + historicalData.slice(index, index + this.batchSize).map( + this.table.getRowValues.bind(this.table, limitEvaluator)) + ); this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, index + this.batchSize, - rowData, limitEvaluator )); } @@ -265,12 +273,10 @@ define( function makeTableRows(object, historicalData) { // Only process one request at a time if (requestTime === this.lastRequestTime) { - console.log('Processing request'); var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData.call(this, historicalData, 0, [], limitEvaluator); + processData.call(this, historicalData, 0, limitEvaluator); } else { - console.log('Ignoring returned data because of staleness'); - resolve([]); + resolve(rowData); } } @@ -287,7 +293,6 @@ define( objects.forEach(requestData.bind(this)); } else { scope.loading = false; - console.log('Resolved promise'); resolve([]); } }.bind(this)); @@ -317,16 +322,15 @@ define( this.$scope.$broadcast('remove:row', 0); this.$scope.rows.shift(); } - - this.$scope.$broadcast('add:row', - this.$scope.rows.length - 1); - + if (!this.$scope.loading) { + this.$scope.$broadcast('add:row', + this.$scope.rows.length - 1); + } } objects.forEach(function (object){ this.subscriptions.push( telemetryApi.subscribe(object, newData.bind(this, object), {})); - console.log('subscribed'); }.bind(this)); return objects; @@ -374,7 +378,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - //.then(this.subscribeToNewData) + .then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; From 0c3ff82cfefdff624f5352ac8192844a38385639 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 17 Jan 2017 14:44:09 -0800 Subject: [PATCH 06/12] [Table] Added ticking to combined historical/real-time table Don't add duplicate telemetry data --- .../utcTimeSystem/src/UTCTimeSystem.js | 2 +- .../table/res/templates/telemetry-table.html | 2 +- .../features/table/src/TableConfiguration.js | 3 +- .../features/table/src/TelemetryCollection.js | 114 ++++++++++++++++++ .../src/controllers/MCTTableController.js | 70 ++++++----- .../controllers/TelemetryTableController.js | 62 +++++----- .../features/table/src/directives/MCTTable.js | 1 + 7 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 platform/features/table/src/TelemetryCollection.js diff --git a/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js b/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js index 1c4e317682..671be1bfff 100644 --- a/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js +++ b/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js @@ -25,7 +25,7 @@ define([ '../../core/src/timeSystems/LocalClock' ], function (TimeSystem, LocalClock) { var FIFTEEN_MINUTES = 15 * 60 * 1000, - DEFAULT_PERIOD = 1000; + DEFAULT_PERIOD = 100; /** * This time system supports UTC dates and provides a ticking clock source. diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html index 310225b47c..24c6a7702f 100644 --- a/platform/features/table/res/templates/telemetry-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -5,7 +5,7 @@ headers="headers" rows="rows" time-columns="tableController.timeColumns" - on-show-cell="" + format-cell="formatCell" enableFilter="true" enableSort="true" auto-scroll="autoScroll" diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index a63ea569d6..2ba908953d 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -66,7 +66,8 @@ define( return { cssClass: alarm && alarm.cssClass, text: formatter ? formatter.format(telemetryDatum[metadatum.key]) - : telemetryDatum[metadatum.key] + : telemetryDatum[metadatum.key], + value: telemetryDatum[metadatum.key] } } }); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js new file mode 100644 index 0000000000..16bf3a27ba --- /dev/null +++ b/platform/features/table/src/TelemetryCollection.js @@ -0,0 +1,114 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + ['lodash'], + function (_) { + function TelemetryCollection() { + this.telemetry = []; + this.sortField = undefined; + this.lastBounds = {}; + + _.bindAll(this,[ + 'iteratee' + ]); + } + + TelemetryCollection.prototype.iteratee = function (element) { + return _.get(element, this.sortField); + }; + + TelemetryCollection.prototype.bounds = function (bounds) { + var startChanged = this.lastBounds.start !== bounds.start; + var endChanged = this.lastBounds.end !== bounds.end; + var fromStart = 0; + var fromEnd = 0; + var discarded = []; + + if (startChanged){ + var testValue = _.set({}, this.sortField, bounds.start); + fromStart = _.sortedIndex(this.telemetry, testValue, this.sortField); + discarded = this.telemetry.splice(0, fromStart); + } + if (endChanged) { + var testValue = _.set({}, this.sortField, bounds.end); + fromEnd = _.sortedLastIndex(this.telemetry, testValue, this.sortField); + discarded = discarded.concat(this.telemetry.splice(fromEnd, this.telemetry.length - fromEnd)); + } + this.lastBounds = bounds; + return discarded; + }; + + TelemetryCollection.prototype.isValid = function (element) { + var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); + var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && + _.get(element, this.sortField) <= this.lastBounds.end; + + return noBoundsDefined || withinBounds; + }; + + TelemetryCollection.prototype.add = function (element) { + //console.log('data: ' + element.Time.value); + if (this.isValid(element)){ + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. + + var isDuplicate = false; + var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + + if (startIx !== this.telemetry.length) { + var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = this.telemetry.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + } + + if (!isDuplicate) { + this.telemetry.splice(startIx, 0, element); + return true; + } else { + return false; + } + + } else { + return false; + } + }; + + TelemetryCollection.prototype.clear = function () { + this.telemetry = undefined; + }; + + TelemetryCollection.prototype.sort = function (sortField){ + this.sortField = sortField; + this.telemetry = _.sortBy(this.telemetry, this.iteratee); + }; + + return TelemetryCollection; + } +); diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index aef6e68240..e347ca048f 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -35,7 +35,7 @@ define( 'changeTimeSystem', 'scrollToBottom', 'addRow', - 'removeRow', + 'removeRows', 'onScroll', 'firstVisible', 'lastVisible', @@ -126,7 +126,7 @@ define( * Listen for rows added individually (eg. for real-time tables) */ $scope.$on('add:row', this.addRow); - $scope.$on('remove:row', this.removeRow); + $scope.$on('remove:rows', this.removeRows); /** * Populated from the default-sort attribute on MctTable @@ -229,39 +229,47 @@ define( * `remove:row` broadcast event. * @private */ - MCTTableController.prototype.removeRow = function (event, rowIndex) { - var row = this.$scope.rows[rowIndex], - // Do a sequential search here. Only way of finding row is by - // object equality, so array is in effect unsorted. + MCTTableController.prototype.removeRows = function (event, rows) { + var indexInDisplayRows; + rows.forEach(function (row){ + // Do a sequential search here. Only way of finding row is by + // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); - if (indexInDisplayRows !== -1) { - this.$scope.displayRows.splice(indexInDisplayRows, 1); - this.setVisibleRows(); - } + if (indexInDisplayRows !== -1) { + this.$scope.displayRows.splice(indexInDisplayRows, 1); + } + }, this); + + this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows)); + + this.setElementSizes(); + this.setVisibleRows() + .then(function () { + if (this.$scope.autoScroll) { + this.scrollToBottom(); + } + }.bind(this)); + }; /** * @private */ MCTTableController.prototype.onScroll = function (event) { - if (!this.scrolling) { - this.scrolling = true; + requestAnimationFrame(function () { + this.setVisibleRows(); + this.digest(); - requestAnimationFrame(function () { - this.setVisibleRows(); - this.digest(); - - // If user scrolls away from bottom, disable auto-scroll. - // Auto-scroll will be re-enabled if user scrolls to bottom again. - if (this.scrollable[0].scrollTop < - (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { - this.$scope.autoScroll = false; - } else { - this.$scope.autoScroll = true; - } - this.scrolling = false; - }.bind(this)); + // If user scrolls away from bottom, disable auto-scroll. + // Auto-scroll will be re-enabled if user scrolls to bottom again. + if (this.scrollable[0].scrollTop < + (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { + this.$scope.autoScroll = false; + } else { + this.$scope.autoScroll = true; } + this.scrolling = false; + }.bind(this)); }; /** @@ -339,13 +347,19 @@ define( this.$scope.visibleRows[0].rowIndex === start && this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { - - return Promise.resolve(); // don't update if no changes are required. + return this.digest(); + //return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) .map(function (row, i) { +/* var formattedRow = JSON.parse(JSON.stringify(row)); + if (self.$scope.formatCell) { + Object.keys(formattedRow).forEach(function (header) { + formattedRow[header].text = self.$scope.formatCell(header, row[header].text); + }); + } */ return { rowIndex: start + i, offsetY: ((start + i) * self.$scope.rowHeight) + diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index f015d11cab..bd98ad6976 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -27,10 +27,11 @@ define( [ '../TableConfiguration', - '../../../../../src/api/objects/object-utils' + '../../../../../src/api/objects/object-utils', + '../TelemetryCollection' ], - function (TableConfiguration, objectUtils) { + function (TableConfiguration, objectUtils, TelemetryCollection) { /** * The TableController is responsible for getting data onto the page @@ -62,6 +63,7 @@ define( openmct); this.lastBounds = this.openmct.conductor.bounds(); this.requestTime = 0; + this.telemetry = new TelemetryCollection(); /* * Create a new format object from legacy object, and replace it @@ -136,18 +138,8 @@ define( this.openmct.conductor.on('bounds', this.changeBounds); }; - TelemetryTableController.prototype.tick = function (bounds) { - // Can't do ticking until we change how data is handled - // Pass raw values to table, with format function - - /*if (this.$scope.defaultSort) { - this.$scope.rows.filter(function (row){ - return row[] - }) - }*/ - }; - TelemetryTableController.prototype.changeBounds = function (bounds) { + //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && @@ -157,10 +149,16 @@ define( (bounds.start !== this.lastBounds.start || bounds.end !== this.lastBounds.end); + var discarded = this.telemetry.bounds(bounds); + + if (discarded.length > 0){ + this.$scope.$broadcast('remove:rows', discarded); + } + if (isTick){ // Treat it as a realtime tick // Drop old data that falls outside of bounds - this.tick(bounds); + //this.tick(bounds); } else if (isDeltaChange){ // No idea... // Historical query for bounds, then tick on @@ -214,11 +212,13 @@ define( var allColumns = telemetryApi.commonValuesForHints(metadatas, []); this.table.populateColumns(allColumns); - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { return metadatum.name; }); + // For now, use first time field for time conductor + this.telemetry.sort(this.timeColumns[0] + '.value'); + this.filterColumns(); var timeSystem = this.openmct.conductor.timeSystem(); @@ -241,12 +241,13 @@ define( var rowData = []; var processedObjects = 0; var requestTime = this.lastRequestTime = Date.now(); + var telemetryCollection = this.telemetry; return new Promise(function (resolve, reject){ - function finishProcessing(tableRows){ - scope.rows = tableRows.concat(scope.rows); + function finishProcessing(){ + scope.rows = telemetryCollection.telemetry; scope.loading = false; - resolve(tableRows); + resolve(scope.rows); } function processData(historicalData, index, limitEvaluator){ @@ -254,13 +255,14 @@ define( processedObjects++; if (processedObjects === objects.length) { - finishProcessing(rowData); + finishProcessing(); } } else { - rowData = rowData.concat( - historicalData.slice(index, index + this.batchSize).map( - this.table.getRowValues.bind(this.table, limitEvaluator)) - ); + historicalData.slice(index, index + this.batchSize) + .forEach(function (datum) { + telemetryCollection.add(this.table.getRowValues( + limitEvaluator, datum)); + }.bind(this)); this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, @@ -305,8 +307,12 @@ define( */ TelemetryTableController.prototype.subscribeToNewData = function (objects) { var telemetryApi = this.openmct.telemetry; + var telemetryCollection = this.telemetry; //Set table max length to avoid unbounded growth. - var maxRows = 100000; + //var maxRows = 100000; + var maxRows = Number.MAX_VALUE; + var limitEvaluator; + var added = false; this.subscriptions.forEach(function (subscription) { subscription(); @@ -314,15 +320,15 @@ define( this.subscriptions = []; function newData(domainObject, datum) { - this.$scope.rows.push(this.table.getRowValues( - telemetryApi.limitEvaluator(domainObject), datum)); + limitEvaluator = telemetryApi.limitEvaluator(domainObject); + added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum)); //Inform table that a new row has been added if (this.$scope.rows.length > maxRows) { - this.$scope.$broadcast('remove:row', 0); + this.$scope.$broadcast('remove:rows', this.$scope.rows[0]); this.$scope.rows.shift(); } - if (!this.$scope.loading) { + if (!this.$scope.loading && added) { this.$scope.$broadcast('add:row', this.$scope.rows.length - 1); } diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index b240fa7f43..70a2b6665c 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -94,6 +94,7 @@ define( scope: { headers: "=", rows: "=", + formatCell: "=?", enableFilter: "=?", enableSort: "=?", autoScroll: "=?", From ae2b73a4f5a004bd05d62f00488ac9b8b81e2f6d Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 19 Jan 2017 21:18:53 -0800 Subject: [PATCH 07/12] [Tables] Increase default table size --- .../features/table/src/TelemetryCollection.js | 104 +++++++++++------- .../src/controllers/MCTTableController.js | 2 +- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index 16bf3a27ba..d5816b37b6 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -21,44 +21,72 @@ *****************************************************************************/ define( - ['lodash'], - function (_) { + [ + 'lodash', + 'eventEmitter' + ], + function (_, eventEmitter) { function TelemetryCollection() { + eventEmitter.call(this, arguments); this.telemetry = []; + this.forwardBuffer = []; this.sortField = undefined; this.lastBounds = {}; + this.lastStartIndex = 0; + this.lastEndIndex = 0; _.bindAll(this,[ 'iteratee' ]); } + TelemetryCollection.prototype = Object.create(eventEmitter.prototype); + TelemetryCollection.prototype.iteratee = function (element) { return _.get(element, this.sortField); }; - TelemetryCollection.prototype.bounds = function (bounds) { + /** + * This function is optimized for ticking - it assumes that start and end bounds + * will only increase and as such this cannot be used for decreasing bounds changes. + * For arbitrary bounds changes, it's assumed that a telemetry requery is performed anyway, and the + * collection is cleared and repopulated. + * @param bounds + */ + TelemetryCollection.prototype.tick = function (bounds) { var startChanged = this.lastBounds.start !== bounds.start; var endChanged = this.lastBounds.end !== bounds.end; - var fromStart = 0; - var fromEnd = 0; - var discarded = []; + var startIndex = 0; + var endIndex = 0; + var discarded = undefined; + var added = undefined; if (startChanged){ var testValue = _.set({}, this.sortField, bounds.start); - fromStart = _.sortedIndex(this.telemetry, testValue, this.sortField); - discarded = this.telemetry.splice(0, fromStart); + // Calculate the new index of the first element within the bounds + startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField); + discarded = this.telemetry.slice(this.lastStartIndex, startIndex + 1); } if (endChanged) { var testValue = _.set({}, this.sortField, bounds.end); - fromEnd = _.sortedLastIndex(this.telemetry, testValue, this.sortField); - discarded = discarded.concat(this.telemetry.splice(fromEnd, this.telemetry.length - fromEnd)); + // Calculate the new index of the last element in bounds + endIndex = _.sortedLastIndex(this.telemetry, testValue, this.sortField); + added = this.telemetry.slice(this.lastEndIndex, endIndex + 1); + } + + if (discarded.length > 0){ + this.emit('discarded', discarded); + } + if (added.length > 0){ + this.emit('added', added); } this.lastBounds = bounds; - return discarded; }; - TelemetryCollection.prototype.isValid = function (element) { + /*collection.on('added'); + collection.on('discarded');*/ + + TelemetryCollection.prototype.inBounds = function (element) { var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; @@ -66,37 +94,37 @@ define( return noBoundsDefined || withinBounds; }; + //Todo: addAll for initial historical data TelemetryCollection.prototype.add = function (element) { - //console.log('data: ' + element.Time.value); - if (this.isValid(element)){ - // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // based on time stamp because the array is guaranteed ordered due - // to sorted insertion. + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. - var isDuplicate = false; - var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + var isDuplicate = false; + var startIx = _.sortedIndex(this.telemetry, element, this.sortField); - if (startIx !== this.telemetry.length) { - var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + if (startIx !== this.telemetry.length) { + var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); - // Create an array of potential dupes, based on having the - // same time stamp - var potentialDupes = this.telemetry.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = this.telemetry.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + } + + if (!isDuplicate) { + this.telemetry.splice(startIx, 0, element); + + if (this.inBounds(element)) { + // If new element is within bounds, then the index within the + // master of the last element in bounds has just increased by one. + this.lastEndIndex++; + //If the new element is within bounds, add it immediately + this.emit('added', [element]); } - - if (!isDuplicate) { - this.telemetry.splice(startIx, 0, element); - return true; - } else { - return false; - } - - } else { - return false; } }; diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index e347ca048f..7ae11eae0c 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -18,7 +18,7 @@ define( this.$scope = $scope; this.element = $(element[0]); this.$window = $window; - this.maxDisplayRows = 50; + this.maxDisplayRows = 100; this.scrollable = this.element.find('.l-view-section.scrolling').first(); this.resultsHeader = this.element.find('.mct-table>thead').first(); From 6cd99efbb972ef0476a08659d7357b4a2fdaf6ed Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 23 Jan 2017 12:43:59 -0800 Subject: [PATCH 08/12] [Tables] Added telemetry buffer so that subscription data is not discarded if it's beyond the end bounds --- example/generator/src/generatorWorker.js | 1 + .../features/table/src/TelemetryCollection.js | 118 +++++++++++------- .../src/controllers/MCTTableController.js | 58 ++++----- .../controllers/TelemetryTableController.js | 71 ++++++----- .../controllers/MCTTableControllerSpec.js | 18 +-- 5 files changed, 152 insertions(+), 114 deletions(-) diff --git a/example/generator/src/generatorWorker.js b/example/generator/src/generatorWorker.js index 091297e185..bbf1851346 100644 --- a/example/generator/src/generatorWorker.js +++ b/example/generator/src/generatorWorker.js @@ -52,6 +52,7 @@ function onSubscribe(message) { var data = message.data; + // Keep var start = Date.now(); var step = 1000 / data.dataRateInHz; var nextStep = start - (start % step) + step; diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index d5816b37b6..185c1549af 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -23,24 +23,23 @@ define( [ 'lodash', - 'eventEmitter' + 'EventEmitter' ], - function (_, eventEmitter) { + function (_, EventEmitter) { function TelemetryCollection() { - eventEmitter.call(this, arguments); + EventEmitter.call(this, arguments); this.telemetry = []; - this.forwardBuffer = []; + this.highBuffer = []; this.sortField = undefined; this.lastBounds = {}; - this.lastStartIndex = 0; - this.lastEndIndex = 0; _.bindAll(this,[ + 'addOne', 'iteratee' ]); } - TelemetryCollection.prototype = Object.create(eventEmitter.prototype); + TelemetryCollection.prototype = Object.create(EventEmitter.prototype); TelemetryCollection.prototype.iteratee = function (element) { return _.get(element, this.sortField); @@ -53,7 +52,7 @@ define( * collection is cleared and repopulated. * @param bounds */ - TelemetryCollection.prototype.tick = function (bounds) { + TelemetryCollection.prototype.bounds = function (bounds) { var startChanged = this.lastBounds.start !== bounds.start; var endChanged = this.lastBounds.end !== bounds.end; var startIndex = 0; @@ -65,71 +64,106 @@ define( var testValue = _.set({}, this.sortField, bounds.start); // Calculate the new index of the first element within the bounds startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField); - discarded = this.telemetry.slice(this.lastStartIndex, startIndex + 1); + discarded = this.telemetry.splice(0, startIndex); } if (endChanged) { var testValue = _.set({}, this.sortField, bounds.end); // Calculate the new index of the last element in bounds - endIndex = _.sortedLastIndex(this.telemetry, testValue, this.sortField); - added = this.telemetry.slice(this.lastEndIndex, endIndex + 1); + endIndex = _.sortedLastIndex(this.highBuffer, testValue, this.sortField); + added = this.highBuffer.splice(0, endIndex); + this.telemetry = this.telemetry.concat(added); } - if (discarded.length > 0){ + if (discarded && discarded.length > 0){ this.emit('discarded', discarded); } - if (added.length > 0){ + if (added && added.length > 0) { this.emit('added', added); } this.lastBounds = bounds; }; - /*collection.on('added'); - collection.on('discarded');*/ - TelemetryCollection.prototype.inBounds = function (element) { var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); - var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && + var withinBounds = + _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; return noBoundsDefined || withinBounds; }; - //Todo: addAll for initial historical data - TelemetryCollection.prototype.add = function (element) { - // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // based on time stamp because the array is guaranteed ordered due - // to sorted insertion. - + /** + * @private + * @param element + */ + TelemetryCollection.prototype.addOne = function (element) { var isDuplicate = false; - var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + var boundsDefined = this.lastBounds && (this.lastBounds.start && this.lastBounds.end); + var array; - if (startIx !== this.telemetry.length) { - var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + // Insert into either in-bounds array, or the out of bounds high buffer. + // Data in the high buffer will be re-evaluated for possible insertion on next tick - // Create an array of potential dupes, based on having the - // same time stamp - var potentialDupes = this.telemetry.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + if (boundsDefined) { + var boundsHigh = _.get(element, this.sortField) > this.lastBounds.end; + var boundsLow = _.get(element, this.sortField) < this.lastBounds.start; + + if (!boundsHigh && !boundsLow) { + array = this.telemetry; + } else if (boundsHigh) { + array = this.highBuffer; + } + } else { + array = this.highBuffer; } - if (!isDuplicate) { - this.telemetry.splice(startIx, 0, element); + // If out of bounds low, disregard data + if (!boundsLow) { + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. - if (this.inBounds(element)) { - // If new element is within bounds, then the index within the - // master of the last element in bounds has just increased by one. - this.lastEndIndex++; - //If the new element is within bounds, add it immediately - this.emit('added', [element]); + var startIx = _.sortedIndex(array, element, this.sortField); + + if (startIx !== array.length) { + var endIx = _.sortedLastIndex(array, element, this.sortField); + + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = array.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; } + + if (!isDuplicate) { + array.splice(startIx, 0, element); + + //Return true if it was added and in bounds + return array === this.telemetry; + } + } + return false; + }; + + TelemetryCollection.prototype.addAll = function (elements) { + var added = elements.filter(this.addOne); + this.emit('added', added); + }; + + //Todo: addAll for initial historical data + TelemetryCollection.prototype.add = function (element) { + if (this.addOne(element)){ + this.emit('added', [element]); + return true; + } else { + return false; } }; TelemetryCollection.prototype.clear = function () { - this.telemetry = undefined; + this.telemetry = []; }; TelemetryCollection.prototype.sort = function (sortField){ diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 7ae11eae0c..af942f8b55 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -31,32 +31,32 @@ define( //Bind all class functions to 'this' _.bindAll(this, [ - 'destroyConductorListeners', - 'changeTimeSystem', - 'scrollToBottom', - 'addRow', - 'removeRows', - 'onScroll', - 'firstVisible', - 'lastVisible', - 'setVisibleRows', - 'setHeaders', - 'setElementSizes', + 'addRows', 'binarySearch', - 'insertSorted', - 'sortComparator', - 'sortRows', 'buildLargestRow', - 'resize', - 'filterAndSort', - 'setRows', - 'filterRows', - 'scrollToRow', - 'setTimeOfInterestRow', - 'changeTimeOfInterest', 'changeBounds', + 'changeTimeOfInterest', + 'changeTimeSystem', + 'destroyConductorListeners', + 'digest', + 'filterAndSort', + 'filterRows', + 'firstVisible', + 'insertSorted', + 'lastVisible', 'onRowClick', - 'digest' + 'onScroll', + 'removeRows', + 'resize', + 'scrollToBottom', + 'scrollToRow', + 'setElementSizes', + 'setHeaders', + 'setRows', + 'setTimeOfInterestRow', + 'setVisibleRows', + 'sortComparator', + 'sortRows' ]); this.scrollable.on('scroll', this.onScroll); @@ -125,7 +125,7 @@ define( /* * Listen for rows added individually (eg. for real-time tables) */ - $scope.$on('add:row', this.addRow); + $scope.$on('add:rows', this.addRows); $scope.$on('remove:rows', this.removeRows); /** @@ -199,16 +199,13 @@ define( * `add:row` broadcast event. * @private */ - MCTTableController.prototype.addRow = function (event, rowIndex) { - var row = this.$scope.rows[rowIndex]; - + MCTTableController.prototype.addRows = function (event, rows) { //Does the row pass the current filter? - if (this.filterRows([row]).length === 1) { - //Insert the row into the correct position in the array - this.insertSorted(this.$scope.displayRows, row); + if (this.filterRows(rows).length > 0) { + rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows)); //Resize the columns , then update the rows visible in the table - this.resize([this.$scope.sizingRow, row]) + this.resize([this.$scope.sizingRow].concat(rows)) .then(this.setVisibleRows) .then(function () { if (this.$scope.autoScroll) { @@ -220,7 +217,6 @@ define( if (toi !== -1) { this.setTimeOfInterestRow(toi); } - } }; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index bd98ad6976..d4414b1004 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -79,7 +79,9 @@ define( 'getHistoricalData', 'subscribeToNewData', 'changeBounds', - 'setScroll' + 'setScroll', + 'addRowsToTable', + 'removeRowsFromTable', ]); this.getData(); @@ -88,6 +90,9 @@ define( this.openmct.conductor.on('follow', this.setScroll); this.setScroll(this.openmct.conductor.follow()); + this.telemetry.on('added', this.addRowsToTable); + this.telemetry.on('discarded', this.removeRowsFromTable); + this.$scope.$on("$destroy", this.destroy); } @@ -102,16 +107,19 @@ define( */ TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { var scope = this.$scope; + var sortColumn = undefined; scope.defaultSort = undefined; + if (timeSystem) { this.table.columns.forEach(function (column) { if (column.metadata.key === timeSystem.metadata.key) { - scope.defaultSort = column.getTitle(); + sortColumn = column; } }); - this.$scope.rows = _.sortBy(this.$scope.rows, function (row) { - return row[this.$scope.defaultSort]; - }); + if (sortColumn) { + scope.defaultSort = sortColumn.getTitle(); + this.telemetry.sort(sortColumn.getTitle() + '.value'); + } } }; @@ -138,31 +146,23 @@ define( this.openmct.conductor.on('bounds', this.changeBounds); }; + TelemetryTableController.prototype.addRowsToTable = function (rows) { + this.$scope.$broadcast('add:rows', rows); + }; + + TelemetryTableController.prototype.removeRowsFromTable = function (rows) { + this.$scope.$broadcast('remove:rows', rows); + }; + TelemetryTableController.prototype.changeBounds = function (bounds) { //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && bounds.end !== this.lastBounds.end; - var isDeltaChange = follow && - !isTick && - (bounds.start !== this.lastBounds.start || - bounds.end !== this.lastBounds.end); - - var discarded = this.telemetry.bounds(bounds); - - if (discarded.length > 0){ - this.$scope.$broadcast('remove:rows', discarded); - } if (isTick){ - // Treat it as a realtime tick - // Drop old data that falls outside of bounds - //this.tick(bounds); - } else if (isDeltaChange){ - // No idea... - // Historical query for bounds, then tick on - this.getData(); + this.telemetry.bounds(bounds); } else { // Is fixed bounds change this.getData(); @@ -212,19 +212,23 @@ define( var allColumns = telemetryApi.commonValuesForHints(metadatas, []); this.table.populateColumns(allColumns); - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { + + var domainColumns = telemetryApi.commonValuesForHints(metadatas, ['x']); + this.timeColumns = domainColumns.map(function (metadatum) { return metadatum.name; }); - // For now, use first time field for time conductor - this.telemetry.sort(this.timeColumns[0] + '.value'); - this.filterColumns(); var timeSystem = this.openmct.conductor.timeSystem(); if (timeSystem) { this.sortByTimeSystem(timeSystem); } + if (!this.telemetry.sortColumn && domainColumns.length > 0) { + this.telemetry.sort(domainColumns[0].name + '.value'); + } + + } return objects; }; @@ -245,6 +249,7 @@ define( return new Promise(function (resolve, reject){ function finishProcessing(){ + telemetryCollection.addAll(rowData); scope.rows = telemetryCollection.telemetry; scope.loading = false; resolve(scope.rows); @@ -258,11 +263,9 @@ define( finishProcessing(); } } else { - historicalData.slice(index, index + this.batchSize) - .forEach(function (datum) { - telemetryCollection.add(this.table.getRowValues( - limitEvaluator, datum)); - }.bind(this)); + rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) + .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, @@ -300,6 +303,7 @@ define( }.bind(this)); }; + /** * @private * @param objects @@ -348,6 +352,9 @@ define( var scope = this.$scope; var newObject = this.newObject; + this.telemetry.clear(); + this.telemetry.bounds(this.openmct.conductor.bounds()); + this.$scope.loading = true; function error(e) { @@ -384,7 +391,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - .then(this.subscribeToNewData) + //.then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 61e4a2eece..970ca72047 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -465,20 +465,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); //Add a duplicate row mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -494,12 +494,12 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -513,11 +513,11 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -536,7 +536,7 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row7); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); From ef8efbd53dd70c25581ade706e669f0601d8426d Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 25 Jan 2017 15:41:08 -0800 Subject: [PATCH 09/12] [Tables] Default UTC time system if available and none others defined --- .../conductor/utcTimeSystem/bundle.js | 19 ++++++++++++++++++- .../features/table/src/TelemetryCollection.js | 4 ++-- .../controllers/TelemetryTableController.js | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/platform/features/conductor/utcTimeSystem/bundle.js b/platform/features/conductor/utcTimeSystem/bundle.js index df9a6c0d38..806087e2ea 100644 --- a/platform/features/conductor/utcTimeSystem/bundle.js +++ b/platform/features/conductor/utcTimeSystem/bundle.js @@ -22,7 +22,8 @@ define([ "./src/UTCTimeSystem", - 'legacyRegistry' + 'legacyRegistry', + 'openmct' ], function ( UTCTimeSystem, legacyRegistry @@ -34,7 +35,23 @@ define([ "implementation": UTCTimeSystem, "depends": ["$timeout"] } + ], + "runs": [ + { + "implementation": function (openmct, $timeout) { + // Temporary shim to initialize the time conductor to + // something + if (!openmct.conductor.timeSystem()) { + var utcTimeSystem = new UTCTimeSystem($timeout); + + openmct.conductor.timeSystem(utcTimeSystem, utcTimeSystem.defaults().bounds); + } + }, + "depends": ["openmct", "$timeout"], + "priority": "fallback" + } ] } }); + }); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index 185c1549af..4a5f277b66 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -84,7 +84,7 @@ define( }; TelemetryCollection.prototype.inBounds = function (element) { - var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); + var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined); var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; @@ -114,7 +114,7 @@ define( array = this.highBuffer; } } else { - array = this.highBuffer; + array = this.telemetry; } // If out of bounds low, disregard data diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index d4414b1004..7e8fe1fe29 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -391,7 +391,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - //.then(this.subscribeToNewData) + .then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; From a3311e4c5783467f9c692987a4091857b671c080 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 26 Jan 2017 10:59:22 -0800 Subject: [PATCH 10/12] [Tables] Tests and style fixes --- .../conductor/utcTimeSystem/bundle.js | 3 +- .../table/res/templates/telemetry-table.html | 1 - .../features/table/src/TableConfiguration.js | 47 +-- .../features/table/src/TelemetryCollection.js | 165 ++++++-- .../src/controllers/MCTTableController.js | 44 +- .../controllers/TelemetryTableController.js | 231 +++++++---- .../features/table/test/DomainColumnSpec.js | 80 ---- .../features/table/test/NameColumnSpec.js | 56 --- .../features/table/test/RangeColumnSpec.js | 74 ---- .../table/test/TableConfigurationSpec.js | 142 +++---- .../table/test/TelemetryCollectionSpec.js | 191 +++++++++ .../HistoricalTableControllerSpec.js | 380 ------------------ .../controllers/MCTTableControllerSpec.js | 100 ++--- .../RealtimeTableControllerSpec.js | 171 -------- .../TelemetryTableControllerSpec.js | 364 +++++++++++++++++ 15 files changed, 970 insertions(+), 1079 deletions(-) delete mode 100644 platform/features/table/test/DomainColumnSpec.js delete mode 100644 platform/features/table/test/NameColumnSpec.js delete mode 100644 platform/features/table/test/RangeColumnSpec.js create mode 100644 platform/features/table/test/TelemetryCollectionSpec.js delete mode 100644 platform/features/table/test/controllers/HistoricalTableControllerSpec.js delete mode 100644 platform/features/table/test/controllers/RealtimeTableControllerSpec.js create mode 100644 platform/features/table/test/controllers/TelemetryTableControllerSpec.js diff --git a/platform/features/conductor/utcTimeSystem/bundle.js b/platform/features/conductor/utcTimeSystem/bundle.js index 806087e2ea..5db4bd968f 100644 --- a/platform/features/conductor/utcTimeSystem/bundle.js +++ b/platform/features/conductor/utcTimeSystem/bundle.js @@ -22,8 +22,7 @@ define([ "./src/UTCTimeSystem", - 'legacyRegistry', - 'openmct' + "legacyRegistry" ], function ( UTCTimeSystem, legacyRegistry diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html index 24c6a7702f..5bda288f1c 100644 --- a/platform/features/table/res/templates/telemetry-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -1,6 +1,5 @@
- Request time: {{tableController.lastRequestTime | date : 'HH:mm:ss:sss'}} 0){ + if (discarded && discarded.length > 0) { + /** + * A `discarded` event is thrown when telemetry data fall out of + * bounds due to a bounds change event + * @type {object[]} discarded the telemetry data + * discarded as a result of the bounds change + */ this.emit('discarded', discarded); } if (added && added.length > 0) { + /** + * An `added` event is thrown when a bounds change results in + * received telemetry falling within the new bounds. + * @type {object[]} added the telemetry data that is now within bounds + */ this.emit('added', added); } this.lastBounds = bounds; }; - TelemetryCollection.prototype.inBounds = function (element) { + /** + * Determines is a given telemetry datum is within the bounds currently + * defined for this telemetry collection. + * @private + * @param datum + * @returns {boolean} + */ + TelemetryCollection.prototype.inBounds = function (datum) { var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined); var withinBounds = - _.get(element, this.sortField) >= this.lastBounds.start && - _.get(element, this.sortField) <= this.lastBounds.end; + _.get(datum, this.sortField) >= this.lastBounds.start && + _.get(datum, this.sortField) <= this.lastBounds.end; return noBoundsDefined || withinBounds; }; /** + * Adds an individual item to the collection. Used internally only * @private - * @param element + * @param item */ - TelemetryCollection.prototype.addOne = function (element) { + TelemetryCollection.prototype.addOne = function (item) { var isDuplicate = false; - var boundsDefined = this.lastBounds && (this.lastBounds.start && this.lastBounds.end); + var boundsDefined = this.lastBounds && + (this.lastBounds.start !== undefined && this.lastBounds.end !== undefined); var array; + var boundsLow; + var boundsHigh; + + // If collection is not sorted by a time field, we cannot respond to + // bounds events, so no bounds checking necessary + if (this.sortField === undefined) { + this.telemetry.push(item); + return true; + } // Insert into either in-bounds array, or the out of bounds high buffer. // Data in the high buffer will be re-evaluated for possible insertion on next tick if (boundsDefined) { - var boundsHigh = _.get(element, this.sortField) > this.lastBounds.end; - var boundsLow = _.get(element, this.sortField) < this.lastBounds.start; + boundsHigh = _.get(item, this.sortField) > this.lastBounds.end; + boundsLow = _.get(item, this.sortField) < this.lastBounds.start; if (!boundsHigh && !boundsLow) { array = this.telemetry; @@ -119,26 +166,26 @@ define( // If out of bounds low, disregard data if (!boundsLow) { + // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it + // items around the given time. Use sortedIndex because it // employs a binary search which is O(log n). Can use binary search // based on time stamp because the array is guaranteed ordered due // to sorted insertion. - - var startIx = _.sortedIndex(array, element, this.sortField); + var startIx = _.sortedIndex(array, item, this.sortField); if (startIx !== array.length) { - var endIx = _.sortedLastIndex(array, element, this.sortField); + var endIx = _.sortedLastIndex(array, item, this.sortField); // Create an array of potential dupes, based on having the // same time stamp var potentialDupes = array.slice(startIx, endIx + 1); // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1; } if (!isDuplicate) { - array.splice(startIx, 0, element); + array.splice(startIx, 0, item); //Return true if it was added and in bounds return array === this.telemetry; @@ -147,28 +194,60 @@ define( return false; }; - TelemetryCollection.prototype.addAll = function (elements) { - var added = elements.filter(this.addOne); + /** + * Add an array of objects to this telemetry collection + * @fires TelemetryCollection#added + * @param {object[]} items + */ + TelemetryCollection.prototype.add = function (items) { + var added = items.filter(this.addOne); this.emit('added', added); }; - //Todo: addAll for initial historical data - TelemetryCollection.prototype.add = function (element) { - if (this.addOne(element)){ - this.emit('added', [element]); - return true; - } else { - return false; - } - }; - + /** + * Clears the contents of the telemetry collection + */ TelemetryCollection.prototype.clear = function () { this.telemetry = []; }; - TelemetryCollection.prototype.sort = function (sortField){ + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sort("value"); + * + * // Sort by ms since epoch + * collection.sort("timestamp.ms"); + * + * // Sort by formatted date text + * collection.sort("timestamp.text"); + * + * + * @param {string} sortField An object property path. + */ + TelemetryCollection.prototype.sort = function (sortField) { this.sortField = sortField; - this.telemetry = _.sortBy(this.telemetry, this.iteratee); + if (sortField !== undefined) { + this.telemetry = _.sortBy(this.telemetry, this.iteratee); + } }; return TelemetryCollection; diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index af942f8b55..c987a0c3b8 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -1,7 +1,10 @@ define( - ['zepto'], - function ($) { + [ + 'zepto', + 'lodash' + ], + function ($, _) { /** * A controller for the MCTTable directive. Populates scope with @@ -134,7 +137,7 @@ define( */ $scope.$watch('defaultSort', function (newColumn, oldColumn) { if (newColumn !== oldColumn) { - $scope.toggleSort(newColumn) + $scope.toggleSort(newColumn); } }); @@ -163,7 +166,7 @@ define( } }.bind(this)); - $scope.$on('$destroy', function() { + $scope.$on('$destroy', function () { this.scrollable.off('scroll', this.onScroll); this.destroyConductorListeners(); @@ -172,7 +175,7 @@ define( delete this.$scope; }.bind(this)); - }; + } MCTTableController.prototype.destroyConductorListeners = function () { this.conductor.off('timeSystem', this.changeTimeSystem); @@ -227,7 +230,7 @@ define( */ MCTTableController.prototype.removeRows = function (event, rows) { var indexInDisplayRows; - rows.forEach(function (row){ + rows.forEach(function (row) { // Do a sequential search here. Only way of finding row is by // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); @@ -252,7 +255,7 @@ define( * @private */ MCTTableController.prototype.onScroll = function (event) { - requestAnimationFrame(function () { + this.$window.requestAnimationFrame(function () { this.setVisibleRows(); this.digest(); @@ -344,18 +347,11 @@ define( this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { return this.digest(); - //return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) .map(function (row, i) { -/* var formattedRow = JSON.parse(JSON.stringify(row)); - if (self.$scope.formatCell) { - Object.keys(formattedRow).forEach(function (header) { - formattedRow[header].text = self.$scope.formatCell(header, row[header].text); - }); - } */ return { rowIndex: start + i, offsetY: ((start + i) * self.$scope.rowHeight) + @@ -585,19 +581,20 @@ define( MCTTableController.prototype.digest = function () { var scope = this.$scope; var self = this; - var requestAnimationFrame = this.$window.requestAnimationFrame; + var raf = this.$window.requestAnimationFrame; + var promise = this.digestPromise; - if (!this.digestPromise) { - this.digestPromise = new Promise(function (resolve) { - requestAnimationFrame(function() { + if (!promise) { + self.digestPromise = promise = new Promise(function (resolve) { + raf(function () { scope.$digest(); - delete self.digestPromise; + self.digestPromise = undefined; resolve(); }); }); } - return this.digestPromise; + return promise; }; /** @@ -640,8 +637,10 @@ define( } this.$scope.displayRows = this.filterAndSort(newRows || []); - this.resize(newRows) - .then(this.setVisibleRows) + return this.resize(newRows) + .then(function (rows) { + return this.setVisibleRows(rows); + }.bind(this)) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. .then(function () { @@ -692,6 +691,7 @@ define( }; /** + * Scroll the view to a given row index * @param displayRowIndex {number} The index in the displayed rows * to scroll to. */ diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 7e8fe1fe29..5beb49cb7e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +/* global console*/ /** * This bundle adds a table view for displaying telemetry data. @@ -28,10 +29,11 @@ define( [ '../TableConfiguration', '../../../../../src/api/objects/object-utils', - '../TelemetryCollection' + '../TelemetryCollection', + 'lodash' ], - function (TableConfiguration, objectUtils, TelemetryCollection) { + function (TableConfiguration, objectUtils, TelemetryCollection, _) { /** * The TableController is responsible for getting data onto the page @@ -46,6 +48,7 @@ define( $timeout, openmct ) { + this.$scope = $scope; this.$timeout = $timeout; this.openmct = openmct; @@ -55,14 +58,14 @@ define( * Initialization block */ this.columns = {}; //Range and Domain columns - this.deregisterListeners = []; + this.unobserveObject = undefined; this.subscriptions = []; this.timeColumns = []; $scope.rows = []; this.table = new TableConfiguration($scope.domainObject, openmct); this.lastBounds = this.openmct.conductor.bounds(); - this.requestTime = 0; + this.lastRequestTime = 0; this.telemetry = new TelemetryCollection(); /* @@ -81,38 +84,45 @@ define( 'changeBounds', 'setScroll', 'addRowsToTable', - 'removeRowsFromTable', + 'removeRowsFromTable' ]); - this.getData(); - this.registerChangeListeners(); + // Retrieve data when domain object is available. + // Also deferring telemetry request makes testing easier as controller + // construction has no unintended consequences. + $scope.$watch("domainObject", function () { + this.getData(); + this.registerChangeListeners(); + }.bind(this)); - this.openmct.conductor.on('follow', this.setScroll); this.setScroll(this.openmct.conductor.follow()); - this.telemetry.on('added', this.addRowsToTable); - this.telemetry.on('discarded', this.removeRowsFromTable); - this.$scope.$on("$destroy", this.destroy); } - TelemetryTableController.prototype.setScroll = function (scroll){ + /** + * @private + * @param {boolean} scroll + */ + TelemetryTableController.prototype.setScroll = function (scroll) { this.$scope.autoScroll = scroll; }; /** * Based on the selected time system, find a matching domain column * to sort by. By default will just match on key. - * @param timeSystem + * + * @private + * @param {TimeSystem} timeSystem */ TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { var scope = this.$scope; - var sortColumn = undefined; + var sortColumn; scope.defaultSort = undefined; if (timeSystem) { this.table.columns.forEach(function (column) { - if (column.metadata.key === timeSystem.metadata.key) { + if (column.getKey() === timeSystem.metadata.key) { sortColumn = column; } }); @@ -124,44 +134,66 @@ define( }; /** - * Attach listeners to domain object to respond to changes due to - * composition, etc. + * Attaches listeners that respond to state change in domain object, + * conductor, and receipt of telemetry + * * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); - this.deregisterListeners = []; + if (this.unobserveObject) { + this.unobserveObject(); + } - this.deregisterListeners.push( - this.openmct.objects.observe(this.newObject, "*", - function (domainObject){ + this.unobserveObject = this.openmct.objects.observe(this.newObject, "*", + function (domainObject) { this.newObject = domainObject; this.getData(); }.bind(this) - ) - ); + ); + this.openmct.conductor.on('timeSystem', this.sortByTimeSystem); this.openmct.conductor.on('bounds', this.changeBounds); + this.openmct.conductor.on('follow', this.setScroll); + + this.telemetry.on('added', this.addRowsToTable); + this.telemetry.on('discarded', this.removeRowsFromTable); }; + /** + * On receipt of new telemetry, informs mct-table directive that new rows + * are available and passes populated rows to it + * + * @private + * @param rows + */ TelemetryTableController.prototype.addRowsToTable = function (rows) { this.$scope.$broadcast('add:rows', rows); }; + /** + * When rows are to be removed, informs mct-table directive. Row removal + * happens when rows call outside the bounds of the time conductor + * + * @private + * @param rows + */ TelemetryTableController.prototype.removeRowsFromTable = function (rows) { this.$scope.$broadcast('remove:rows', rows); }; + /** + * On Time Conductor bounds change, update displayed telemetry. In the + * case of a tick, previously visible telemetry that is now out of band + * will be removed from the table. + * @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds + */ TelemetryTableController.prototype.changeBounds = function (bounds) { - //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && bounds.end !== this.lastBounds.end; - if (isTick){ + if (isTick) { this.telemetry.bounds(bounds); } else { // Is fixed bounds change @@ -171,7 +203,7 @@ define( }; /** - * Release the current subscription (called when scope is destroyed) + * Clean controller, deregistering listeners etc. */ TelemetryTableController.prototype.destroy = function () { @@ -182,11 +214,11 @@ define( this.subscriptions.forEach(function (subscription) { subscription(); }); - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); + + if (this.unobserveObject) { + this.unobserveObject(); + } this.subscriptions = []; - this.deregisterListeners = []; if (this.timeoutHandle) { this.$timeout.cancel(this.timeoutHandle); @@ -200,9 +232,10 @@ define( }; /** + * For given objects, populate column metadata and table headers. * @private - * @param objects - * @returns {*} + * @param {module:openmct.DomainObject[]} objects the domain objects for + * which columns should be populated */ TelemetryTableController.prototype.loadColumns = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -220,25 +253,28 @@ define( this.filterColumns(); + // Default to no sort on underlying telemetry collection. Sorting + // is necessary to do bounds filtering, but this is only possible + // if data matches selected time system + this.telemetry.sort(undefined); + var timeSystem = this.openmct.conductor.timeSystem(); if (timeSystem) { this.sortByTimeSystem(timeSystem); } - if (!this.telemetry.sortColumn && domainColumns.length > 0) { - this.telemetry.sort(domainColumns[0].name + '.value'); - } - } return objects; }; /** + * Request telemetry data from an historical store for given objects. * @private - * @param objects The domain objects to request telemetry for - * @returns {*|{configFile}|app|boolean|Route|Object} + * @param {object[]} The domain objects to request telemetry for + * @returns {Promise} resolved when historical data is available */ TelemetryTableController.prototype.getHistoricalData = function (objects) { + var self = this; var openmct = this.openmct; var bounds = openmct.conductor.bounds(); var scope = this.$scope; @@ -247,15 +283,22 @@ define( var requestTime = this.lastRequestTime = Date.now(); var telemetryCollection = this.telemetry; - return new Promise(function (resolve, reject){ - function finishProcessing(){ - telemetryCollection.addAll(rowData); + var promise = new Promise(function (resolve, reject) { + /* + * On completion of batched processing, set the rows on scope + */ + function finishProcessing() { + telemetryCollection.add(rowData); scope.rows = telemetryCollection.telemetry; scope.loading = false; + resolve(scope.rows); } - function processData(historicalData, index, limitEvaluator){ + /* + * Process a batch of historical data + */ + function processData(historicalData, index, limitEvaluator) { if (index >= historicalData.length) { processedObjects++; @@ -263,51 +306,57 @@ define( finishProcessing(); } } else { - rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) - .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + rowData = rowData.concat(historicalData.slice(index, index + self.batchSize) + .map(self.table.getRowValues.bind(self.table, limitEvaluator))); - this.timeoutHandle = this.$timeout(processData.bind( - this, - historicalData, - index + this.batchSize, - limitEvaluator - )); + /* + Use timeout to yield process to other UI activities. On + return, process next batch + */ + self.timeoutHandle = self.$timeout(function () { + processData(historicalData, index + self.batchSize, limitEvaluator); + }); } } function makeTableRows(object, historicalData) { - // Only process one request at a time - if (requestTime === this.lastRequestTime) { + // Only process the most recent request + if (requestTime === self.lastRequestTime) { var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData.call(this, historicalData, 0, limitEvaluator); + processData(historicalData, 0, limitEvaluator); } else { resolve(rowData); } } - function requestData (object) { + /* + Use the telemetry API to request telemetry for a given object + */ + function requestData(object) { return openmct.telemetry.request(object, { start: bounds.start, end: bounds.end - }).then(makeTableRows.bind(this, object)) + }).then(makeTableRows.bind(undefined, object)) .catch(reject); } this.$timeout.cancel(this.timeoutHandle); - if (objects.length > 0){ - objects.forEach(requestData.bind(this)); + if (objects.length > 0) { + objects.forEach(requestData); } else { scope.loading = false; resolve([]); } }.bind(this)); + + return promise; }; /** + * Subscribe to real-time data for the given objects. * @private - * @param objects - * @returns {*} + * @param {object[]} objects The objects to subscribe to. */ TelemetryTableController.prototype.subscribeToNewData = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -317,6 +366,8 @@ define( var maxRows = Number.MAX_VALUE; var limitEvaluator; var added = false; + var scope = this.$scope; + var table = this.table; this.subscriptions.forEach(function (subscription) { subscription(); @@ -325,20 +376,20 @@ define( function newData(domainObject, datum) { limitEvaluator = telemetryApi.limitEvaluator(domainObject); - added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum)); + added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]); //Inform table that a new row has been added - if (this.$scope.rows.length > maxRows) { - this.$scope.$broadcast('remove:rows', this.$scope.rows[0]); - this.$scope.rows.shift(); + if (scope.rows.length > maxRows) { + scope.$broadcast('remove:rows', scope.rows[0]); + scope.rows.shift(); } - if (!this.$scope.loading && added) { - this.$scope.$broadcast('add:row', - this.$scope.rows.length - 1); + if (!scope.loading && added) { + scope.$broadcast('add:row', + scope.rows.length - 1); } } - objects.forEach(function (object){ + objects.forEach(function (object) { this.subscriptions.push( telemetryApi.subscribe(object, newData.bind(this, object), {})); }.bind(this)); @@ -346,6 +397,12 @@ define( return objects; }; + /** + * Request historical data, and subscribe to for real-time data. + * @private + * @returns {Promise} A promise that is resolved once subscription is + * established, and historical telemetry is received and processed. + */ TelemetryTableController.prototype.getData = function () { var telemetryApi = this.openmct.telemetry; var compositionApi = this.openmct.composition; @@ -359,41 +416,37 @@ define( function error(e) { scope.loading = false; - console.error(e); + console.error(e.stack); } - function filterForTelemetry(objects){ + function filterForTelemetry(objects) { return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi)); } function getDomainObjects() { - return new Promise(function (resolve, reject){ - var objects = [newObject]; - var composition = compositionApi.get(newObject); + var objects = [newObject]; + var composition = compositionApi.get(newObject); - if (composition) { - composition - .load() - .then(function (children) { - return objects.concat(children); - }) - .then(resolve) - .catch(reject); - } else { - return resolve(objects); - } - }); + if (composition) { + return composition + .load() + .then(function (children) { + return objects.concat(children); + }); + } else { + return Promise.resolve(objects); + } } scope.headers = []; scope.rows = []; - getDomainObjects() + return getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) .then(this.subscribeToNewData) .then(this.getHistoricalData) - .catch(error) + .catch(error); }; /** diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js deleted file mode 100644 index 3c144b8427..0000000000 --- a/platform/features/table/test/DomainColumnSpec.js +++ /dev/null @@ -1,80 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/DomainColumn"], - function (DomainColumn) { - - var TEST_DOMAIN_VALUE = "some formatted domain value"; - - describe("A domain column", function () { - var mockDatum, - testMetadata, - mockFormatter, - column; - - beforeEach(function () { - - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name", - format: "Test Format" - }; - mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE); - - column = new DomainColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from domain metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - describe("when given a datum", function () { - beforeEach(function () { - mockDatum = { - testKey: "testKeyValue" - }; - }); - - it("looks up data from the given datum", function () { - expect(column.getValue(undefined, mockDatum)) - .toEqual({ text: TEST_DOMAIN_VALUE }); - }); - - it("uses formatter to format domain values as requested", function () { - column.getValue(undefined, mockDatum); - expect(mockFormatter.formatDomainValue) - .toHaveBeenCalledWith("testKeyValue", "Test Format"); - }); - - }); - - }); - } -); diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js deleted file mode 100644 index 13e858c2ed..0000000000 --- a/platform/features/table/test/NameColumnSpec.js +++ /dev/null @@ -1,56 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/NameColumn"], - function (NameColumn) { - - describe("A name column", function () { - var mockDomainObject, - column; - - beforeEach(function () { - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel"] - ); - mockDomainObject.getModel.andReturn({ - name: "Test object name" - }); - column = new NameColumn(); - }); - - it("reports a column header", function () { - expect(column.getTitle()).toEqual("Name"); - }); - - it("looks up name from an object's model", function () { - expect(column.getValue(mockDomainObject).text) - .toEqual("Test object name"); - }); - - }); - } -); diff --git a/platform/features/table/test/RangeColumnSpec.js b/platform/features/table/test/RangeColumnSpec.js deleted file mode 100644 index 473f26ae56..0000000000 --- a/platform/features/table/test/RangeColumnSpec.js +++ /dev/null @@ -1,74 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/RangeColumn"], - function (RangeColumn) { - - var TEST_RANGE_VALUE = "some formatted range value"; - - describe("A range column", function () { - var testDatum, - testMetadata, - mockFormatter, - mockDomainObject, - column; - - beforeEach(function () { - testDatum = { testKey: 123, otherKey: 456 }; - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name" - }; - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel", "getCapability"] - ); - mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE); - - column = new RangeColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from range metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - it("formats range values as numbers", function () { - expect(column.getValue(mockDomainObject, testDatum).text) - .toEqual(TEST_RANGE_VALUE); - - // Make sure that service interactions were as expected - expect(mockFormatter.formatRangeValue) - .toHaveBeenCalledWith(testDatum.testKey); - expect(mockFormatter.formatDomainValue) - .not.toHaveBeenCalled(); - }); - }); - } -); diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index db90cc1c62..af4f2ee2a8 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -22,13 +22,14 @@ define( [ - "../src/TableConfiguration", - "../src/DomainColumn" + "../src/TableConfiguration" ], - function (Table, DomainColumn) { + function (Table) { describe("A table", function () { var mockDomainObject, + mockAPI, + mockTelemetryAPI, mockTelemetryFormatter, table, mockModel; @@ -41,90 +42,63 @@ define( mockDomainObject.getModel.andReturn(mockModel); mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', [ - 'formatDomainValue', - 'formatRangeValue' + 'format' ]); - mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) { - return valueIn; - }); - mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) { + mockTelemetryFormatter.format.andCallFake(function (valueIn) { return valueIn; }); - table = new Table(mockDomainObject, mockTelemetryFormatter); - }); + mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ + 'getValueFormatter' + ]); + mockAPI = { + telemetry: mockTelemetryAPI + }; + mockTelemetryAPI.getValueFormatter.andReturn(mockTelemetryFormatter); - it("Add column with no index adds new column to the end", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(secondColumn); - table.addColumn(thirdColumn); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); - }); - - it("Add column with index adds new column at the specified" + - " position", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(thirdColumn); - table.addColumn(secondColumn, 1); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); + table = new Table(mockDomainObject, mockAPI); }); describe("Building columns from telemetry metadata", function () { - var metadata = [{ - ranges: [ - { - name: 'Range 1', - key: 'range1' - }, - { - name: 'Range 2', - key: 'range2' + var metadata = [ + { + name: 'Range 1', + key: 'range1', + hints: { + y: 1 } - ], - domains: [ - { - name: 'Domain 1', - key: 'domain1', - format: 'utc' - }, - { - name: 'Domain 2', - key: 'domain2', - format: 'utc' + }, + { + name: 'Range 2', + key: 'range2', + hints: { + y: 2 } - ] - }]; + }, + { + name: 'Domain 1', + key: 'domain1', + format: 'utc', + hints: { + x: 1 + } + }, + { + name: 'Domain 2', + key: 'domain2', + format: 'utc', + hints: { + x: 2 + } + } + ]; beforeEach(function () { table.populateColumns(metadata); }); it("populates columns", function () { - expect(table.columns.length).toBe(5); - }); - - it("Build columns populates columns with domains to the left", function () { - expect(table.columns[1] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[2] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[3] instanceof DomainColumn).toBeFalsy(); + expect(table.columns.length).toBe(4); }); it("Produces headers for each column based on title", function () { @@ -133,7 +107,7 @@ define( spyOn(firstColumn, 'getTitle'); headers = table.getHeaders(); - expect(headers.length).toBe(5); + expect(headers.length).toBe(4); expect(firstColumn.getTitle).toHaveBeenCalled(); }); @@ -170,23 +144,33 @@ define( beforeEach(function () { datum = { - 'range1': 'range 1 value', - 'range2': 'range 2 value', + 'range1': 10, + 'range2': 20, 'domain1': 0, 'domain2': 1 }; - rowValues = table.getRowValues(mockDomainObject, datum); + var limitEvaluator = { + evaluate: function () { + return { + "cssClass": "alarm-class" + }; + } + }; + rowValues = table.getRowValues(limitEvaluator, datum); }); it("Returns a value for every column", function () { expect(rowValues['Range 1'].text).toBeDefined(); - expect(rowValues['Range 1'].text).toEqual('range 1' + - ' value'); + expect(rowValues['Range 1'].text).toEqual(10); }); - it("Uses the telemetry formatter to appropriately format" + + it("Applies appropriate css class if limit violated.", function () { + expect(rowValues['Range 1'].cssClass).toEqual("alarm-class"); + }); + + it("Uses telemetry formatter to appropriately format" + " telemetry values", function () { - expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); + expect(mockTelemetryFormatter.format).toHaveBeenCalled(); }); }); }); diff --git a/platform/features/table/test/TelemetryCollectionSpec.js b/platform/features/table/test/TelemetryCollectionSpec.js new file mode 100644 index 0000000000..014e5c684e --- /dev/null +++ b/platform/features/table/test/TelemetryCollectionSpec.js @@ -0,0 +1,191 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + [ + "../src/TelemetryCollection" + ], + function (TelemetryCollection) { + + describe("A telemetry collection", function () { + + var collection; + var telemetryObjects; + var ms; + var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", + "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"]; + + beforeEach(function () { + telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) { + ms = number * 1000; + return { + timestamp: ms, + value: { + integer: number, + text: integerTextMap[number] + } + }; + }); + collection = new TelemetryCollection(); + }); + + it("Sorts inserted telemetry by specified field", + function () { + collection.sort('value.integer'); + collection.add(telemetryObjects); + expect(collection.telemetry[0].value.integer).toBe(0); + expect(collection.telemetry[1].value.integer).toBe(1); + expect(collection.telemetry[2].value.integer).toBe(2); + expect(collection.telemetry[3].value.integer).toBe(3); + + collection.sort('value.text'); + expect(collection.telemetry[0].value.text).toBe("EIGHT"); + expect(collection.telemetry[1].value.text).toBe("FIVE"); + expect(collection.telemetry[2].value.text).toBe("FOUR"); + expect(collection.telemetry[3].value.text).toBe("NINE"); + } + ); + + describe("on bounds change", function () { + var discardedCallback; + + beforeEach(function () { + discardedCallback = jasmine.createSpy("discarded"); + collection.on("discarded", discardedCallback); + collection.sort("timestamp"); + collection.add(telemetryObjects); + collection.bounds({start: 5000, end: 8000}); + }); + + + it("emits an event indicating that telemetry has " + + "been discarded", function () { + expect(discardedCallback).toHaveBeenCalled(); + }); + + it("discards telemetry data with a time stamp " + + "before specified start bound", function () { + var discarded = discardedCallback.mostRecentCall.args[0]; + + // Expect 5 because as an optimization, the TelemetryCollection + // will not consider telemetry values that exceed the upper + // bounds. Arbitrary bounds changes in which the end bound is + // decreased is assumed to require a new historical query, and + // hence re-population of the collection anyway + expect(discarded.length).toBe(5); + expect(discarded[0].value.integer).toBe(0); + expect(discarded[1].value.integer).toBe(1); + expect(discarded[4].value.integer).toBe(4); + }); + }); + + describe("when adding telemetry to a collection", function () { + var addedCallback; + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + addedCallback = jasmine.createSpy("added"); + collection.on("added", addedCallback); + }); + + it("emits an event", + function () { + var addedObject = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + collection.add([addedObject]); + expect(addedCallback).toHaveBeenCalledWith([addedObject]); + } + ); + it("inserts in the correct order", + function () { + var addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + var addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + collection.add([addedObjectB, addedObjectA]); + + expect(collection.telemetry[11]).toBe(addedObjectB); + } + ); + }); + + describe("buffers telemetry", function () { + var addedObjectA; + var addedObjectB; + + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + + addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + + collection.bounds({start: 0, end: 10000}); + collection.add([addedObjectA, addedObjectB]); + }); + it("when it falls outside of bounds", function () { + expect(collection.highBuffer).toBeDefined(); + expect(collection.highBuffer.length).toBe(1); + expect(collection.highBuffer[0]).toBe(addedObjectB); + }); + it("and adds it to collection when it falls within bounds", function () { + expect(collection.telemetry.length).toBe(11); + collection.bounds({start: 0, end: 11000}); + expect(collection.telemetry.length).toBe(12); + expect(collection.telemetry[11]).toBe(addedObjectB); + }); + it("and removes it from the buffer when it falls within bounds", function () { + expect(collection.highBuffer.length).toBe(1); + collection.bounds({start: 0, end: 11000}); + expect(collection.highBuffer.length).toBe(0); + }); + }); + }); + } +); diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js deleted file mode 100644 index 39f7d1a8f5..0000000000 --- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js +++ /dev/null @@ -1,380 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -define( - [ - "../../src/controllers/HistoricalTableController" - ], - function (TableController) { - - describe('The Table Controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - mockAngularTimeout, - mockTimeoutHandle, - watches, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - function getCallback(target, event) { - return target.calls.filter(function (call) { - return call.args[0] === event; - })[0].args[1]; - } - - beforeEach(function () { - watches = {}; - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection' - ]); - - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockTimeoutHandle = jasmine.createSpy("timeoutHandle"); - mockAngularTimeout = jasmine.createSpy("$timeout"); - mockAngularTimeout.andReturn(mockTimeoutHandle); - mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout"); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'request', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'getMetadata', - 'getSeries', - 'unsubscribe', - 'makeDatum' - ]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getTelemetryObjects.andReturn([]); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj("conductor", [ - "timeSystem", - "on", - "off" - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, - mockTelemetryFormatter, mockAngularTimeout, {conductor: mockConductor}); - - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('subscribes to telemetry handler for telemetry updates', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalled(); - expect(mockTelemetryHandle.request).toHaveBeenCalled(); - }); - - it('Unsubscribes from telemetry when scope is destroyed', function () { - controller.handle = mockTelemetryHandle; - watches.$destroy(); - expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); - }); - - describe('makes use of the table', function () { - - it('to create column definitions from telemetry' + - ' metadata', function () { - controller.setup(); - expect(mockTable.populateColumns).toHaveBeenCalled(); - }); - - it('to create column configuration, which is written to the' + - ' object model', function () { - controller.setup(); - expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); - }); - }); - - it('updates the rows on scope when historical telemetry is received', function () { - var mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }, - mockRow = {'domain': 'Domain Value', 'range': 'Range' + - ' Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - // Angular timeout is called a minumum of twice, regardless - // of batch size used. - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - - it('filters the visible columns based on configuration', function () { - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(3); - expect(controller.$scope.headers[2]).toEqual('domain1'); - - mockConfiguration.domain1 = false; - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(2); - expect(controller.$scope.headers[2]).toBeUndefined(); - }); - - describe('creates event listeners', function () { - beforeEach(function () { - spyOn(controller, 'subscribe'); - spyOn(controller, 'filterColumns'); - }); - - it('triggers telemetry subscription update when domain' + - ' object changes', function () { - controller.registerChangeListeners(); - //'watches' object is populated by fake scope watch and - // watchCollection functions defined above - expect(watches.domainObject).toBeDefined(); - watches.domainObject(mockDomainObject); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when domain' + - ' object composition changes', function () { - controller.registerChangeListeners(); - expect(watches['domainObject.getModel().composition']).toBeDefined(); - watches['domainObject.getModel().composition']([], []); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when time' + - ' conductor bounds change', function () { - controller.registerChangeListeners(); - expect(watches['telemetry:display:bounds']).toBeDefined(); - watches['telemetry:display:bounds'](); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers refiltering of the columns when configuration' + - ' changes', function () { - controller.setup(); - expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined(); - watches['domainObject.getModel().configuration.table.columns'](); - expect(controller.filterColumns).toHaveBeenCalled(); - }); - - }); - describe('After populating columns', function () { - var metadata; - beforeEach(function () { - metadata = [{domains: [{name: 'time domain 1'}, {name: 'time domain 2'}]}, {domains: [{name: 'time domain 3'}, {name: 'time domain 4'}]}]; - controller.populateColumns(metadata); - }); - - it('Automatically identifies time columns', function () { - expect(controller.timeColumns.length).toBe(4); - expect(controller.timeColumns[0]).toBe('time domain 1'); - }); - - it('Automatically sorts by time column that matches current' + - ' time system', function () { - var key = 'time_domain_1', - name = 'time domain 1', - mockTimeSystem = { - metadata: { - key: key - } - }; - - mockTable.columns = [ - { - domainMetadata: { - key: key - }, - getTitle: function () { - return name; - } - }, - { - domainMetadata: { - key: 'anotherColumn' - }, - getTitle: function () { - return 'some other column'; - } - }, - { - domainMetadata: { - key: 'thirdColumn' - }, - getTitle: function () { - return 'a third column'; - } - } - ]; - - expect(mockConductor.on).toHaveBeenCalledWith('timeSystem', jasmine.any(Function)); - getCallback(mockConductor.on, 'timeSystem')(mockTimeSystem); - expect(controller.$scope.defaultSort).toBe(name); - }); - }); - describe('Yields thread', function () { - var mockSeries, - mockRow; - - beforeEach(function () { - mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }; - mockRow = {'domain': 'Domain Value', 'range': 'Range Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - }); - it('when row count exceeds batch size', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Timeout is called a minimum of two times - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - //Because it yields, timeout will have been called a - // third time for the batch. - expect(mockAngularTimeout.calls.length).toEqual(3); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - it('cancelling any outstanding timeouts', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - }); - it('cancels timeout on scope destruction', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Destroy is used by parent class as well, so multiple - // calls are made to scope.$on - var destroyCalls = mockScope.$on.calls.filter(function (call) { - return call.args[0] === '$destroy'; - }); - //Call destroy function - expect(destroyCalls.length).toEqual(2); - - destroyCalls[0].args[1](); - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 970ca72047..f57b981c50 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -39,21 +39,13 @@ define( var controller, mockScope, watches, - mockTimeout, + mockWindow, mockElement, mockExportService, mockConductor, mockFormatService, mockFormat; - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - function getCallback(target, event) { return target.calls.filter(function (call) { return call.args[0] === event; @@ -66,7 +58,8 @@ define( mockScope = jasmine.createSpyObj('scope', [ '$watch', '$on', - '$watchCollection' + '$watchCollection', + '$digest' ]); mockScope.$watchCollection.andCallFake(function (event, callback) { watches[event] = callback; @@ -86,8 +79,11 @@ define( ]); mockScope.displayHeaders = true; - mockTimeout = jasmine.createSpy('$timeout'); - mockTimeout.andReturn(promise(undefined)); + mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']); + mockWindow.requestAnimationFrame.andCallFake(function (f) { + return f(); + }); + mockFormat = jasmine.createSpyObj('formatter', [ 'parse', 'format' @@ -99,7 +95,7 @@ define( controller = new MCTTableController( mockScope, - mockTimeout, + mockWindow, mockElement, mockExportService, mockFormatService, @@ -114,12 +110,12 @@ define( expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function)); }); - it('destroys listeners on destruction', function () { - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners); + it('unregisters listeners on destruction', function () { + expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); getCallback(mockScope.$on, '$destroy')(); expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem); - expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.setTimeOfInterest); + expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest); expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds); }); @@ -233,9 +229,20 @@ define( //Mock setting the rows on scope var rowsCallback = getCallback(mockScope.$watch, 'rows'); - rowsCallback(rowsAsc); + var setRowsPromise = rowsCallback(rowsAsc); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.toiRowIndex).toBe(2); + }); - expect(mockScope.toiRowIndex).toBe(2); }); }); @@ -287,7 +294,7 @@ define( }); it('Supports adding rows individually', function () { - var addRowFunc = getCallback(mockScope.$on, 'add:row'), + var addRowFunc = getCallback(mockScope.$on, 'add:rows'), row4 = { 'col1': {'text': 'row3 col1'}, 'col2': {'text': 'ghi'}, @@ -296,15 +303,15 @@ define( controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); - addRowFunc(undefined, 3); + addRowFunc(undefined, [row4]); expect(mockScope.displayRows.length).toBe(4); }); it('Supports removing rows individually', function () { - var removeRowFunc = getCallback(mockScope.$on, 'remove:row'); + var removeRowFunc = getCallback(mockScope.$on, 'remove:rows'); controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); - removeRowFunc(undefined, 2); + removeRowFunc(undefined, [testRows[2]]); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); }); @@ -366,7 +373,7 @@ define( it('Allows sort column to be changed externally by ' + 'setting or changing sortBy attribute', function () { mockScope.displayRows = testRows; - var sortByCB = getCallback(mockScope.$watch, 'sortColumn'); + var sortByCB = getCallback(mockScope.$watch, 'defaultSort'); sortByCB('col2'); expect(mockScope.sortDirection).toEqual('asc'); @@ -381,10 +388,21 @@ define( it('updates visible rows in scope', function () { var oldRows; mockScope.rows = testRows; - controller.setRows(testRows); + var setRowsPromise = controller.setRows(testRows); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); oldRows = mockScope.visibleRows; mockScope.toggleSort('col2'); - expect(mockScope.visibleRows).not.toEqual(oldRows); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.visibleRows).not.toEqual(oldRows); + }); }); it('correctly sorts rows of differing types', function () { @@ -464,21 +482,10 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); - mockScope.rows.push(row4); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row4, row5, row6, row6]); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); - - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); - - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); - - //Add a duplicate row - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + expect(mockScope.displayRows[6].col2.text).toEqual('aaa'); + //Added a duplicate row expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -493,13 +500,11 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.displayRows = controller.filterRows(testRows); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -512,12 +517,10 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -535,8 +538,7 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row7); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row7]); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); diff --git a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js deleted file mode 100644 index bf29c3d7bd..0000000000 --- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js +++ /dev/null @@ -1,171 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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. - *****************************************************************************/ - -define( - [ - "../../src/controllers/RealtimeTableController" - ], - function (TableController) { - - describe('The real-time table controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - watches, - mockTableRow, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - beforeEach(function () { - watches = {}; - mockTableRow = {'col1': 'val1', 'col2': 'row2'}; - - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection', - '$digest', - '$broadcast' - ]); - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - mockTable.getRowValues.andReturn(mockTableRow); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - mockDomainObject.getCapability.andReturn( - { - getMetadata: function () { - return {ranges: [{format: 'string'}]}; - } - }); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'getMetadata', - 'unsubscribe', - 'getDatum', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'request', - 'getMetadata' - ]); - - // Arbitrary array with non-zero length, contents are not - // used by mocks - mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.getDatum.andReturn({}); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj('conductor', [ - 'on', - 'off', - 'bounds', - 'timeSystem', - 'timeOfInterest' - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, {conductor: mockConductor}); - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('registers for streaming telemetry', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true); - }); - - describe('receives new telemetry', function () { - beforeEach(function () { - controller.subscribe(); - mockScope.rows = []; - }); - - it('updates table with new streaming telemetry', function () { - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0); - }); - it('observes the row limit', function () { - var i = 0; - controller.maxRows = 10; - - //Fill rows array with elements - for (; i < 10; i++) { - mockScope.rows.push({row: i}); - } - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.rows.length).toBe(controller.maxRows); - expect(mockScope.rows[mockScope.rows.length - 1]).toBe(mockTableRow); - expect(mockScope.rows[0].row).toBe(1); - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js new file mode 100644 index 0000000000..4f403edc32 --- /dev/null +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -0,0 +1,364 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + [ + '../../src/controllers/TelemetryTableController', + '../../../../../src/api/objects/object-utils', + 'lodash' + ], + function (TelemetryTableController, objectUtils, _) { + + describe('The TelemetryTableController', function () { + + var controller, + mockScope, + mockTimeout, + mockConductor, + mockAPI, + mockDomainObject, + mockTelemetryAPI, + mockObjectAPI, + mockCompositionAPI, + unobserve, + mockBounds; + + function getCallback(target, event) { + return target.calls.filter(function (call) { + return call.args[0] === event; + })[0].args[1]; + } + + beforeEach(function () { + mockBounds = { + start: 0, + end: 10 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "bounds", + "follow", + "on", + "off", + "timeSystem" + ]); + mockConductor.bounds.andReturn(mockBounds); + mockConductor.follow.andReturn(false); + + mockDomainObject = jasmine.createSpyObj("domainObject", [ + "getModel", + "getId", + "useCapability" + ]); + mockDomainObject.getModel.andReturn({}); + mockDomainObject.getId.andReturn("mockId"); + mockDomainObject.useCapability.andReturn(true); + + mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [ + "get" + ]); + + mockObjectAPI = jasmine.createSpyObj("objectAPI", [ + "observe" + ]); + unobserve = jasmine.createSpy("unobserve"); + mockObjectAPI.observe.andReturn(unobserve); + + mockScope = jasmine.createSpyObj("scope", [ + "$on", + "$watch", + "$broadcast" + ]); + mockScope.domainObject = mockDomainObject; + + mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [ + "canProvideTelemetry", + "subscribe", + "getMetadata", + "commonValuesForHints", + "request", + "limitEvaluator", + "getValueFormatter" + ]); + mockTelemetryAPI.commonValuesForHints.andReturn([]); + mockTelemetryAPI.request.andReturn(Promise.resolve([])); + + + mockTelemetryAPI.canProvideTelemetry.andReturn(false); + + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(1); // Return something + mockTimeout.cancel = jasmine.createSpy("cancel"); + + mockAPI = { + conductor: mockConductor, + objects: mockObjectAPI, + telemetry: mockTelemetryAPI, + composition: mockCompositionAPI + }; + controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI); + }); + + describe('listens for', function () { + beforeEach(function () { + controller.registerChangeListeners(); + }); + it('object mutation', function () { + var calledObject = mockObjectAPI.observe.mostRecentCall.args[0]; + + expect(mockObjectAPI.observe).toHaveBeenCalled(); + expect(calledObject.identifier.key).toEqual(mockDomainObject.getId()); + }); + it('conductor changes', function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function)); + }); + }); + + describe('deregisters all listeners on scope destruction', function () { + var timeSystemListener, + boundsListener, + followListener; + + beforeEach(function () { + controller.registerChangeListeners(); + + timeSystemListener = getCallback(mockConductor.on, "timeSystem"); + boundsListener = getCallback(mockConductor.on, "bounds"); + followListener = getCallback(mockConductor.on, "follow"); + + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + }); + + it('object mutation', function () { + expect(unobserve).toHaveBeenCalled(); + }); + it('conductor changes', function () { + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener); + expect(mockConductor.off).toHaveBeenCalledWith("follow", followListener); + }); + }); + + describe ('Subscribes to new data', function () { + var mockComposition, + mockTelemetryObject, + mockChildren, + unsubscribe, + done; + + beforeEach(function () { + mockComposition = jasmine.createSpyObj("composition", [ + "load" + ]); + + mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [ + "something" + ]); + mockTelemetryObject.identifier = { + key: "mockTelemetryObject" + }; + + unsubscribe = jasmine.createSpy("unsubscribe"); + mockTelemetryAPI.subscribe.andReturn(unsubscribe); + + mockChildren = [mockTelemetryObject]; + mockComposition.load.andReturn(Promise.resolve(mockChildren)); + mockCompositionAPI.get.andReturn(mockComposition); + + mockTelemetryAPI.canProvideTelemetry.andCallFake(function (obj) { + return obj.identifier.key === mockTelemetryObject.identifier.key; + }); + + done = false; + controller.getData().then(function () { + done = true; + }); + }); + + it('fetches historical data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object)); + }); + }); + + it('fetches historical data for the time period specified by the conductor bounds', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds); + }); + }); + + it('subscribes to new data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {}); + }); + + }); + it('and unsubscribes on view destruction', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + + expect(unsubscribe).toHaveBeenCalled(); + }); + }); + }); + + it('When in real-time mode, enables auto-scroll', function () { + controller.registerChangeListeners(); + + var followCallback = getCallback(mockConductor.on, "follow"); + //Confirm pre-condition + expect(mockScope.autoScroll).toBeFalsy(); + + //Mock setting the conductor to 'follow' mode + followCallback(true); + expect(mockScope.autoScroll).toBe(true); + }); + + describe('populates table columns', function () { + var domainMetadata; + var allMetadata; + var mockTimeSystem; + + beforeEach(function () { + domainMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }]; + + allMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }, { + key: "column2", + name: "Column 2", + hints: {} + }, { + key: "column3", + name: "Column 3", + hints: {} + }]; + + mockTimeSystem = { + metadata: { + key: "column1" + } + }; + + mockTelemetryAPI.commonValuesForHints.andCallFake(function (metadata, hints) { + if (_.eq(hints, ["x"])) { + return domainMetadata; + } else if (_.eq(hints, [])) { + return allMetadata; + } + }); + + controller.loadColumns([mockDomainObject]); + }); + + it('based on metadata for given objects', function () { + expect(mockScope.headers).toBeDefined(); + expect(mockScope.headers.length).toBeGreaterThan(0); + expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1); + }); + + it('and sorts by column matching time system', function () { + expect(mockScope.defaultSort).not.toEqual("Column 1"); + controller.sortByTimeSystem(mockTimeSystem); + expect(mockScope.defaultSort).toEqual("Column 1"); + }); + + it('batches processing of rows for performance when receiving historical telemetry', function () { + var mockHistoricalData = [ + { + "column1": 1, + "column2": 2, + "column3": 3 + },{ + "column1": 4, + "column2": 5, + "column3": 6 + }, { + "column1": 7, + "column2": 8, + "column3": 9 + } + ]; + controller.batchSize = 2; + mockTelemetryAPI.request.andReturn(Promise.resolve(mockHistoricalData)); + controller.getHistoricalData([mockDomainObject]); + + waitsFor(function () { + return !!controller.timeoutHandle; + }, "first batch to be processed", 100); + + runs(function () { + //Verify that timeout is being used to yield process + expect(mockTimeout).toHaveBeenCalled(); + mockTimeout.mostRecentCall.args[0](); + expect(mockTimeout.calls.length).toBe(2); + mockTimeout.mostRecentCall.args[0](); + expect(mockScope.rows.length).toBe(3); + }); + }); + }); + + it('Removes telemetry rows from table when they fall out of bounds', function () { + var discardedRows = [ + {"column1": "value 1"}, + {"column2": "value 2"}, + {"column3": "value 3"} + ]; + + spyOn(controller.telemetry, "on").andCallThrough(); + + controller.registerChangeListeners(); + expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function)); + var onDiscard = getCallback(controller.telemetry.on, "discarded"); + onDiscard(discardedRows); + expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows); + }); + + }); + }); From 34dc457affc4cf8b7a2f1f736dccd8f7314abcd6 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Feb 2017 15:35:17 -0800 Subject: [PATCH 11/12] [Tables] Restored telemetry datum field 'name'. Fixed bug with default sort not working --- .../src/controllers/MCTTableController.js | 6 ++- .../controllers/TelemetryTableController.js | 4 +- src/api/telemetry/LegacyTelemetryProvider.js | 43 +++++++++++-------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index c987a0c3b8..d9f3a9f680 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -122,7 +122,11 @@ define( $scope.$watchCollection('filters', function () { self.setRows($scope.rows); }); - $scope.$watch('headers', this.setHeaders); + $scope.$watch('headers', function (newHeaders, oldHeaders) { + if (newHeaders !== oldHeaders) { + this.setHeaders(newHeaders); + } + }.bind(this)); $scope.$watch('rows', this.setRows); /* diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 5beb49cb7e..daf042893e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -240,6 +240,8 @@ define( TelemetryTableController.prototype.loadColumns = function (objects) { var telemetryApi = this.openmct.telemetry; + this.$scope.headers = []; + if (objects.length > 0) { var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); var allColumns = telemetryApi.commonValuesForHints(metadatas, []); @@ -437,8 +439,6 @@ define( return Promise.resolve(objects); } } - - scope.headers = []; scope.rows = []; return getDomainObjects() diff --git a/src/api/telemetry/LegacyTelemetryProvider.js b/src/api/telemetry/LegacyTelemetryProvider.js index f76fe99d10..1e8fe50deb 100644 --- a/src/api/telemetry/LegacyTelemetryProvider.js +++ b/src/api/telemetry/LegacyTelemetryProvider.js @@ -29,7 +29,8 @@ define([ * @implements module:openmct.TelemetryAPI~TelemetryProvider * @constructor */ - function LegacyTelemetryProvider(instantiate) { + function LegacyTelemetryProvider(openmct, instantiate) { + this.telemetryApi = openmct.telemetry; this.instantiate = instantiate; } @@ -45,22 +46,28 @@ define([ }; function createDatum(domainObject, metadata, legacySeries, i) { + var datum; + if (legacySeries.getDatum) { - return legacySeries.getDatum(i); + datum = legacySeries.getDatum(i); + } else { + datum = {}; + metadata.valuesForHints(['x']).forEach(function (metadatum) { + datum[metadatum.key] = legacySeries.getDomainValue(i, metadatum.key); + }); + + metadata.valuesForHints(['y']).forEach(function (metadatum) { + datum[metadatum.key] = legacySeries.getRangeValue(i, metadatum.key); + }); } - var datum = {}; - metadata.domains.reduce(function (d, domain) { - d[domain.key] = legacySeries.getDomainValue(i, domain.key); - return d; - }, datum); - - metadata.ranges.reduce(function (d, range) { - d[range.key] = legacySeries.getRangeValue(i, range.key); - return d; - }, datum); - - datum.name = domainObject.name; + /** + * If telemetry metadata defines a 'name' field, and one is not present + * on the datum, add it. + */ + if (metadata.value('name') !== undefined && datum.name === undefined) { + datum.name = domainObject.name; + } return datum; } @@ -93,11 +100,12 @@ define([ * telemetry data. */ LegacyTelemetryProvider.prototype.request = function (domainObject, request) { + var metadata = this.telemetryApi.getMetadata(domainObject); var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); var capability = oldObject.getCapability("telemetry"); return capability.requestData(request).then(function (telemetrySeries) { - return Promise.resolve(adaptSeries(domainObject, capability.getMetadata(), telemetrySeries)); + return Promise.resolve(adaptSeries(domainObject, metadata, telemetrySeries)); }).catch(function (error) { return Promise.reject(error); }); @@ -118,11 +126,12 @@ define([ * @returns {platform|telemetry.TelemetrySubscription|*} */ LegacyTelemetryProvider.prototype.subscribe = function (domainObject, callback, request) { + var metadata = this.telemetryApi.getMetadata(domainObject); var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); var capability = oldObject.getCapability("telemetry"); function callbackWrapper(series) { - callback(createDatum(domainObject, capability.getMetadata(), series, series.getPointCount() - 1)); + callback(createDatum(domainObject, metadata, series, series.getPointCount() - 1)); } return capability.subscribe(callbackWrapper, request); @@ -145,7 +154,7 @@ define([ // Push onto the start of the default providers array so that it's // always the last resort openmct.telemetry.defaultProviders.unshift( - new LegacyTelemetryProvider(instantiate)); + new LegacyTelemetryProvider(openmct, instantiate)); }; }); From 2fa567b98b73652ae69dae160ed8f6fbb54940ec Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Tue, 21 Feb 2017 16:49:39 -0800 Subject: [PATCH 12/12] [Table] Track by index, save the elements --- platform/features/table/res/templates/mct-table.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 7e24be2c43..056e8aac33 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -49,7 +49,7 @@