From 9f9d28deef8d90bcf5c153a3b98e36e4232498ed Mon Sep 17 00:00:00 2001 From: Josh Baldwin Date: Sat, 21 Jan 2017 11:27:20 -0500 Subject: [PATCH 01/38] adding MCT name to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8761314a0b..490385ab9d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -Open MCT is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. +Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/) From 3d3baddd235e920aaa647b41254bdcbae3a95ba9 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 2 Feb 2017 15:08:26 -0800 Subject: [PATCH 02/38] [Tables] Do not persist column configuration for non-editable objects --- platform/features/table/src/TableConfiguration.js | 4 +++- platform/features/table/test/TableConfigurationSpec.js | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 5b463ee7f1..fee22d47bc 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -187,7 +187,9 @@ define( }); //Synchronize column configuration with model - if (configChanged(configuration, defaultConfig)) { + if (this.domainObject.hasCapability('editor') && + this.domainObject.getCapability('editor').isEditContextRoot() && + configChanged(configuration, defaultConfig)) { this.saveColumnConfiguration(configuration); } diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index db90cc1c62..dcac2a9876 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -35,10 +35,18 @@ define( beforeEach(function () { mockDomainObject = jasmine.createSpyObj('domainObject', - ['getModel', 'useCapability', 'getCapability'] + ['getModel', 'useCapability', 'getCapability', 'hasCapability'] ); mockModel = {}; mockDomainObject.getModel.andReturn(mockModel); + mockDomainObject.getCapability.andCallFake(function (name) { + return name === 'editor' && { + isEditContextRoot: function () { + return true; + } + }; + }); + mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', [ 'formatDomainValue', From d3b4ad41c2ea98edc94ea387ed488491a8e43f46 Mon Sep 17 00:00:00 2001 From: Dhrubomoy Das Gupta Date: Thu, 9 Feb 2017 20:54:52 -0500 Subject: [PATCH 03/38] [Documentation] Fixed filename Fixed file name "Platform.md" to "platform.md". "Platform.md" was giving a 404 error when clicked, in github and in the official site as well. --- docs/src/architecture/framework.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/architecture/framework.md b/docs/src/architecture/framework.md index e269e38f61..78adb98158 100644 --- a/docs/src/architecture/framework.md +++ b/docs/src/architecture/framework.md @@ -131,7 +131,7 @@ Keeping that in mind, there are a few useful patterns supported by the framework that are useful to keep in mind. The specific service infrastructure provided by the platform is described -in the [Platform Architecture](Platform.md). +in the [Platform Architecture](platform.md). ## Extension Categories From 77d0134e2e8b043edf270c675d7c84cdf4694cb1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Feb 2017 10:03:05 -0800 Subject: [PATCH 04/38] [Build] Added Bourbon deprecation warning suppression system. --- platform/commonUI/general/res/sass/startup-base.scss | 1 + platform/commonUI/themes/espresso/res/sass/theme-espresso.scss | 2 +- platform/commonUI/themes/snow/res/sass/theme-snow.scss | 2 +- .../conductor/core/res/sass/time-conductor-espresso.scss | 1 + .../features/conductor/core/res/sass/time-conductor-snow.scss | 1 + platform/features/timeline/res/sass/timeline-espresso.scss | 1 + platform/features/timeline/res/sass/timeline-snow.scss | 1 + platform/features/timeline/res/sass/timeline.scss | 1 + 8 files changed, 8 insertions(+), 2 deletions(-) diff --git a/platform/commonUI/general/res/sass/startup-base.scss b/platform/commonUI/general/res/sass/startup-base.scss index 7b4d382ec6..ed29849920 100644 --- a/platform/commonUI/general/res/sass/startup-base.scss +++ b/platform/commonUI/general/res/sass/startup-base.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "logo-and-bg"; diff --git a/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss b/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss index 02f56f462a..514821f3db 100644 --- a/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss +++ b/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../general/res/sass/_mixins"; diff --git a/platform/commonUI/themes/snow/res/sass/theme-snow.scss b/platform/commonUI/themes/snow/res/sass/theme-snow.scss index fed2d2c3e9..78a4af6908 100644 --- a/platform/commonUI/themes/snow/res/sass/theme-snow.scss +++ b/platform/commonUI/themes/snow/res/sass/theme-snow.scss @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../general/res/sass/_mixins"; diff --git a/platform/features/conductor/core/res/sass/time-conductor-espresso.scss b/platform/features/conductor/core/res/sass/time-conductor-espresso.scss index 03bb41dc7e..67624846fc 100644 --- a/platform/features/conductor/core/res/sass/time-conductor-espresso.scss +++ b/platform/features/conductor/core/res/sass/time-conductor-espresso.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../../commonUI/general/res/sass/constants"; @import "../../../../../commonUI/general/res/sass/mixins"; diff --git a/platform/features/conductor/core/res/sass/time-conductor-snow.scss b/platform/features/conductor/core/res/sass/time-conductor-snow.scss index a927791b5c..d989cd2551 100644 --- a/platform/features/conductor/core/res/sass/time-conductor-snow.scss +++ b/platform/features/conductor/core/res/sass/time-conductor-snow.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../../commonUI/general/res/sass/constants"; @import "../../../../../commonUI/general/res/sass/mixins"; diff --git a/platform/features/timeline/res/sass/timeline-espresso.scss b/platform/features/timeline/res/sass/timeline-espresso.scss index cc18bdce06..66ae9c94e6 100644 --- a/platform/features/timeline/res/sass/timeline-espresso.scss +++ b/platform/features/timeline/res/sass/timeline-espresso.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; diff --git a/platform/features/timeline/res/sass/timeline-snow.scss b/platform/features/timeline/res/sass/timeline-snow.scss index 4f18f64765..31a7ee9887 100644 --- a/platform/features/timeline/res/sass/timeline-snow.scss +++ b/platform/features/timeline/res/sass/timeline-snow.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; diff --git a/platform/features/timeline/res/sass/timeline.scss b/platform/features/timeline/res/sass/timeline.scss index f6fe978ef8..1f9a2c4694 100644 --- a/platform/features/timeline/res/sass/timeline.scss +++ b/platform/features/timeline/res/sass/timeline.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; From 6d5530ba9cd87351795b6b97b48efb9e6b927e32 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Dec 2016 12:08:52 -0800 Subject: [PATCH 05/38] [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 06/38] [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} (80%) 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 80% 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 07/38] [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 08/38] [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 09/38] [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) ">
+ 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 10/38] [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 11/38] [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 12/38] [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 13/38] [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 14/38] [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 15/38] [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 fa962b42bc8c68c9e3181e4ce950cb88cd87e4e2 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Mon, 13 Feb 2017 13:12:58 -0800 Subject: [PATCH 16/38] [API] Use proper key format --- src/api/objects/LegacyObjectAPIInterceptor.js | 2 +- src/api/objects/MutableObject.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/objects/LegacyObjectAPIInterceptor.js b/src/api/objects/LegacyObjectAPIInterceptor.js index 666dcfd5c0..18bfaaac79 100644 --- a/src/api/objects/LegacyObjectAPIInterceptor.js +++ b/src/api/objects/LegacyObjectAPIInterceptor.js @@ -43,7 +43,7 @@ define([ var handleLegacyMutation; var handleMutation = function (newStyleObject) { - var keyString = utils.makeKeyString(newStyleObject.key); + var keyString = utils.makeKeyString(newStyleObject.identifier); var oldStyleObject = this.instantiate(utils.toOldFormat(newStyleObject), keyString); // Don't trigger self 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 970acbd56e85cd50485f38cb8b76a270df075ed4 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 12:15:49 -0800 Subject: [PATCH 17/38] [Build] Skip optimize in dev environment Skip optimize in dev environment to speed up project rebuilds. Very helpful when integration testing openmct.js build artifact. --- gulpfile.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 380f233758..d5e1435395 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -69,6 +69,11 @@ var gulp = require('gulp'), } }; +if (process.env.NODE_ENV === 'development') { + options.requirejsOptimize.optimize = 'none'; +} + + gulp.task('scripts', function () { var requirejsOptimize = require('gulp-requirejs-optimize'); var replace = require('gulp-replace-task'); From af9ffaf02dedf95fc80dee1c713592723cf36390 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 10:51:36 -0800 Subject: [PATCH 18/38] Stop loading bundles.json Stop application from requesting bundles.json at first load. This was confusing to some external developers who would see an error in the log and not know the cause. --- platform/framework/src/Constants.js | 1 - platform/framework/src/FrameworkInitializer.js | 4 ++-- platform/framework/src/FrameworkLayer.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/platform/framework/src/Constants.js b/platform/framework/src/Constants.js index 663fde6d72..0ba19f4cd8 100644 --- a/platform/framework/src/Constants.js +++ b/platform/framework/src/Constants.js @@ -25,7 +25,6 @@ */ define({ MODULE_NAME: "OpenMCTWeb", - BUNDLE_LISTING_FILE: "bundles.json", BUNDLE_FILE: "bundle.json", SEPARATOR: "/", EXTENSION_SUFFIX: "[]", diff --git a/platform/framework/src/FrameworkInitializer.js b/platform/framework/src/FrameworkInitializer.js index cea0a6ddb7..4a56998b61 100644 --- a/platform/framework/src/FrameworkInitializer.js +++ b/platform/framework/src/FrameworkInitializer.js @@ -61,8 +61,8 @@ define( * @param bundleList * @returns {*} */ - FrameworkInitializer.prototype.runApplication = function (bundleList) { - return this.loader.loadBundles(bundleList) + FrameworkInitializer.prototype.runApplication = function () { + return this.loader.loadBundles([]) .then(bind(this.resolver.resolveBundles, this.resolver)) .then(bind(this.registrar.registerExtensions, this.registrar)) .then(bind(this.bootstrapper.bootstrap, this.bootstrapper)); diff --git a/platform/framework/src/FrameworkLayer.js b/platform/framework/src/FrameworkLayer.js index 067bf763bc..e84b1e29e0 100644 --- a/platform/framework/src/FrameworkLayer.js +++ b/platform/framework/src/FrameworkLayer.js @@ -98,7 +98,7 @@ define([ // Initialize the application $log.info("Initializing application."); - initializer.runApplication(Constants.BUNDLE_LISTING_FILE); + initializer.runApplication(); }; return FrameworkLayer; From aaedf5d57620e3a9e0b1ff657b4d16b31b9e5c83 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 15:02:39 -0800 Subject: [PATCH 19/38] cssclass is now cssClass Make property name consistent with standard camelCase naming. --- docs/src/tutorials/index.md | 62 +++++++++---------- example/eventGenerator/bundle.js | 2 +- example/export/bundle.js | 2 +- example/generator/bundle.js | 10 +-- example/imagery/bundle.js | 2 +- example/localTimeSystem/src/LADTickSource.js | 2 +- example/msl/bundle.js | 6 +- example/plotOptions/bundle.js | 4 +- example/profiling/src/DigestIndicator.js | 2 +- platform/commonUI/browse/bundle.js | 4 +- .../browse/src/windowing/FullscreenAction.js | 4 +- .../test/windowing/FullscreenActionSpec.js | 2 +- platform/commonUI/edit/bundle.js | 14 ++--- .../res/templates/create/create-menu.html | 4 +- .../res/templates/edit-action-buttons.html | 6 +- .../src/controllers/EditActionController.js | 2 +- .../commonUI/edit/src/creation/AddAction.js | 2 +- .../edit/src/creation/CreateAction.js | 2 +- .../edit/src/creation/CreateWizard.js | 2 +- .../controllers/EditActionControllerSpec.js | 4 +- .../edit/test/creation/CreateActionSpec.js | 2 +- .../res/templates/controls/action-button.html | 2 +- .../res/templates/controls/switcher.html | 4 +- .../res/templates/menu/context-menu.html | 2 +- platform/core/bundle.js | 12 ++-- platform/core/src/actions/ActionAggregator.js | 2 +- platform/core/src/types/TypeImpl.js | 6 +- platform/core/test/types/TypeImplSpec.js | 2 +- platform/core/test/types/TypeProviderSpec.js | 6 +- platform/entanglement/bundle.js | 10 +-- .../src/services/LocationService.js | 2 +- platform/features/clock/bundle.js | 12 ++-- .../clock/src/controllers/TimerController.js | 4 +- .../test/controllers/TimerControllerSpec.js | 6 +- .../templates/mode-selector/mode-menu.html | 4 +- .../core/src/timeSystems/LocalClock.js | 2 +- .../core/src/ui/TimeConductorViewService.js | 6 +- .../utcTimeSystem/src/UTCTimeSystem.js | 4 +- platform/features/fixed/bundle.js | 46 +++++++------- platform/features/imagery/bundle.js | 2 +- platform/features/layout/bundle.js | 54 ++++++++-------- .../layout/src/elements/ElementFactory.js | 2 +- platform/features/pages/bundle.js | 4 +- platform/features/plot/bundle.js | 2 +- .../features/plot/res/templates/plot.html | 4 +- platform/features/plot/src/PlotController.js | 2 +- .../plot/src/modes/PlotModeOptions.js | 6 +- platform/features/static-markup/bundle.js | 2 +- platform/features/table/bundle.js | 8 +-- platform/features/timeline/bundle.js | 26 ++++---- .../forms/res/templates/controls/button.html | 2 +- .../forms/res/templates/controls/color.html | 2 +- .../res/templates/controls/composite.html | 2 +- .../res/templates/controls/menu-button.html | 4 +- .../res/templates/controls/textarea.html | 2 +- .../res/templates/controls/textfield.html | 2 +- platform/forms/res/templates/form.html | 4 +- .../src/controllers/DialogButtonController.js | 2 +- .../controllers/DialogButtonControllerSpec.js | 4 +- .../search/res/templates/search-menu.html | 2 +- .../test/controllers/SearchControllerSpec.js | 2 +- .../controllers/SearchMenuControllerSpec.js | 4 +- src/api/telemetry/TelemetryAPI.js | 2 +- src/api/types/Type.js | 2 +- src/api/types/TypeRegistry.js | 2 +- src/ui/ViewRegistry.js | 2 +- 66 files changed, 212 insertions(+), 212 deletions(-) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index fc5f47971e..e8f4417890 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -320,7 +320,7 @@ define([ + { + "key": "example.todo", + "name": "To-Do List", -+ "cssclass": "icon-check", ++ "cssClass": "icon-check", + "description": "A list of things that need to be done.", + "features": ["creation"] + } @@ -340,7 +340,7 @@ Going through the properties we've defined: domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `cssclass` maps to an icon that will be shown for each To-Do List. The icons +* The `cssClass` maps to an icon that will be shown for each To-Do List. The icons are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss). A complete list of available icons will be provided in the future. * The `description` is also human-readable, and will be used whenever a longer @@ -416,7 +416,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"] } @@ -425,7 +425,7 @@ define([ + { + "key": "example.todo", + "type": "example.todo", -+ "cssclass": "icon-check", ++ "cssClass": "icon-check", + "name": "List", + "templateUrl": "templates/todo.html", + "editable": true @@ -447,7 +447,7 @@ the domain object type, but could have chosen any unique name. domain objects of that type. This means that we'll see this view for To-do Lists that we create, but not for other domain objects (such as Folders.) -* The `cssclass` and `name` properties describe the icon and human-readable name +* The `cssClass` and `name` properties describe the icon and human-readable name for this view to display in the UI where needed (if multiple views are available for To-do Lists, the user will be able to choose one.) @@ -473,7 +473,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], + "model": { @@ -488,7 +488,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true @@ -647,7 +647,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -662,7 +662,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true @@ -741,7 +741,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -756,7 +756,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -766,7 +766,7 @@ define([ + "items": [ + { + "text": "Add Task", -+ "cssclass": "icon-plus", ++ "cssClass": "icon-plus", + "method": "addTask", + "control": "button" + } @@ -775,7 +775,7 @@ define([ + { + "items": [ + { -+ "cssclass": "icon-trash", ++ "cssClass": "icon-trash", + "method": "removeTask", + "control": "button" + } @@ -971,7 +971,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -986,7 +986,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -996,7 +996,7 @@ define([ "items": [ { "text": "Add Task", - "cssclass": "icon-plus", + "cssClass": "icon-plus", "method": "addTask", "control": "button" } @@ -1005,7 +1005,7 @@ define([ { "items": [ { - "cssclass": "icon-trash", + "cssClass": "icon-trash", "method": "removeTask", "control": "button" } @@ -1236,7 +1236,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -1248,7 +1248,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -1258,7 +1258,7 @@ define([ "items": [ { "text": "Add Task", - "cssclass": "icon-plus", + "cssClass": "icon-plus", "method": "addTask", "control": "button" } @@ -1267,7 +1267,7 @@ define([ { "items": [ { - "cssclass": "icon-trash", + "cssClass": "icon-trash", "method": "removeTask", "control": "button" } @@ -1374,7 +1374,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true @@ -1677,7 +1677,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true @@ -1843,7 +1843,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true, @@ -2320,7 +2320,7 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" } ], "roots": [ @@ -2706,18 +2706,18 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" }, + { + "name": "Subsystem", + "key": "example.subsystem", -+ "cssclass": "icon-object", ++ "cssClass": "icon-object", + "model": { "composition": [] } + }, + { + "name": "Measurement", + "key": "example.measurement", -+ "cssclass": "icon-telemetry", ++ "cssClass": "icon-telemetry", + "model": { "telemetry": {} }, + "telemetry": { + "source": "example.source", @@ -3031,18 +3031,18 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" }, { "name": "Subsystem", "key": "example.subsystem", - "cssclass": "icon-object", + "cssClass": "icon-object", "model": { "composition": [] } }, { "name": "Measurement", "key": "example.measurement", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "model": { "telemetry": {} }, "telemetry": { "source": "example.source", diff --git a/example/eventGenerator/bundle.js b/example/eventGenerator/bundle.js index 157fa62542..425de02157 100644 --- a/example/eventGenerator/bundle.js +++ b/example/eventGenerator/bundle.js @@ -49,7 +49,7 @@ define([ { "key": "eventGenerator", "name": "Event Message Generator", - "cssclass": "icon-folder-new", + "cssClass": "icon-folder-new", "description": "For development use. Creates sample event message data that mimics a live data stream.", "priority": 10, "features": "creation", diff --git a/example/export/bundle.js b/example/export/bundle.js index 5322d53a83..bcafee4050 100644 --- a/example/export/bundle.js +++ b/example/export/bundle.js @@ -36,7 +36,7 @@ define([ "name": "Export Telemetry as CSV", "implementation": ExportTelemetryAsCSVAction, "category": "contextual", - "cssclass": "icon-download", + "cssClass": "icon-download", "depends": [ "exportService" ] } ] diff --git a/example/generator/bundle.js b/example/generator/bundle.js index 259c5cff15..c2edb09f23 100644 --- a/example/generator/bundle.js +++ b/example/generator/bundle.js @@ -86,7 +86,7 @@ define([ { "key": "generator", "name": "Sine Wave Generator", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", "priority": 10, "features": "creation", @@ -130,7 +130,7 @@ define([ { "name": "Period", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "period", "required": true, "property": [ @@ -142,7 +142,7 @@ define([ { "name": "Amplitude", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "amplitude", "required": true, "property": [ @@ -154,7 +154,7 @@ define([ { "name": "Offset", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "offset", "required": true, "property": [ @@ -166,7 +166,7 @@ define([ { "name": "Data Rate (hz)", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "dataRateInHz", "required": true, "property": [ diff --git a/example/imagery/bundle.js b/example/imagery/bundle.js index 6232229e40..2e16b1bb3e 100644 --- a/example/imagery/bundle.js +++ b/example/imagery/bundle.js @@ -49,7 +49,7 @@ define([ { "key": "imagery", "name": "Example Imagery", - "cssclass": "icon-image", + "cssClass": "icon-image", "features": "creation", "description": "For development use. Creates example imagery data that mimics a live imagery stream.", "priority": 10, diff --git a/example/localTimeSystem/src/LADTickSource.js b/example/localTimeSystem/src/LADTickSource.js index a4999d6964..f8efc65d04 100644 --- a/example/localTimeSystem/src/LADTickSource.js +++ b/example/localTimeSystem/src/LADTickSource.js @@ -31,7 +31,7 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'], this.metadata = { key: 'test-lad', mode: 'lad', - cssclass: 'icon-clock', + cssClass: 'icon-clock', label: 'Latest Available Data', name: 'Latest available data', description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' diff --git a/example/msl/bundle.js b/example/msl/bundle.js index 12dbbb5157..e40200d13a 100644 --- a/example/msl/bundle.js +++ b/example/msl/bundle.js @@ -41,18 +41,18 @@ define([ { "name":"Mars Science Laboratory", "key": "msl.curiosity", - "cssclass": "icon-object" + "cssClass": "icon-object" }, { "name": "Instrument", "key": "msl.instrument", - "cssclass": "icon-object", + "cssClass": "icon-object", "model": {"composition": []} }, { "name": "Measurement", "key": "msl.measurement", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "model": {"telemetry": {}}, "telemetry": { "source": "rems.source", diff --git a/example/plotOptions/bundle.js b/example/plotOptions/bundle.js index ae47d2a97c..15c420c3f7 100644 --- a/example/plotOptions/bundle.js +++ b/example/plotOptions/bundle.js @@ -81,7 +81,7 @@ define([ { "key": "plot", "name": "Example Telemetry Plot", - "cssclass": "icon-telemetry-panel", + "cssClass": "icon-telemetry-panel", "description": "For development use. A plot for displaying telemetry.", "priority": 10, "delegates": [ @@ -129,7 +129,7 @@ define([ { "name": "Period", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "period", "required": true, "property": [ diff --git a/example/profiling/src/DigestIndicator.js b/example/profiling/src/DigestIndicator.js index 826dd2f23f..5839cca904 100644 --- a/example/profiling/src/DigestIndicator.js +++ b/example/profiling/src/DigestIndicator.js @@ -63,7 +63,7 @@ define( * Get the CSS class that defines the icon * to display in this indicator. This will appear * as a dataflow icon. - * @returns {string} the cssclass of the dataflow icon + * @returns {string} the cssClass of the dataflow icon */ getCssClass: function () { return "icon-connectivity"; diff --git a/platform/commonUI/browse/bundle.js b/platform/commonUI/browse/bundle.js index 3d3d76ee8e..9a883ded23 100644 --- a/platform/commonUI/browse/bundle.js +++ b/platform/commonUI/browse/bundle.js @@ -226,7 +226,7 @@ define([ "$window" ], "group": "windowing", - "cssclass": "icon-new-window", + "cssClass": "icon-new-window", "priority": "preferred" }, { @@ -241,7 +241,7 @@ define([ { "key": "items", "name": "Items", - "cssclass": "icon-thumbs-strip", + "cssClass": "icon-thumbs-strip", "description": "Grid of available items", "template": itemsTemplate, "uses": [ diff --git a/platform/commonUI/browse/src/windowing/FullscreenAction.js b/platform/commonUI/browse/src/windowing/FullscreenAction.js index 975562af0c..5c74ace73e 100644 --- a/platform/commonUI/browse/src/windowing/FullscreenAction.js +++ b/platform/commonUI/browse/src/windowing/FullscreenAction.js @@ -46,12 +46,12 @@ define( }; FullscreenAction.prototype.getMetadata = function () { - // We override getMetadata, because the icon cssclass and + // We override getMetadata, because the icon cssClass and // description need to be determined at run-time // based on whether or not we are currently // full screen. var metadata = Object.create(FullscreenAction); - metadata.cssclass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse"; + metadata.cssClass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse"; metadata.description = screenfull.isFullscreen ? EXIT_FULLSCREEN : ENTER_FULLSCREEN; metadata.group = "windowing"; diff --git a/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js b/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js index 913535fa59..423b9a2eb0 100644 --- a/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js +++ b/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js @@ -51,7 +51,7 @@ define( }); it("provides displayable metadata", function () { - expect(action.getMetadata().cssclass).toBeDefined(); + expect(action.getMetadata().cssClass).toBeDefined(); }); }); diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 75c0269373..d4e63e063b 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -163,7 +163,7 @@ define([ ], "description": "Edit", "category": "view-control", - "cssclass": "major icon-pencil" + "cssClass": "major icon-pencil" }, { "key": "properties", @@ -172,7 +172,7 @@ define([ "view-control" ], "implementation": PropertiesAction, - "cssclass": "major icon-pencil", + "cssClass": "major icon-pencil", "name": "Edit Properties...", "description": "Edit properties of this object.", "depends": [ @@ -183,7 +183,7 @@ define([ "key": "remove", "category": "contextual", "implementation": RemoveAction, - "cssclass": "icon-trash", + "cssClass": "icon-trash", "name": "Remove", "description": "Remove this object from its containing object.", "depends": [ @@ -195,7 +195,7 @@ define([ "category": "save", "implementation": SaveAndStopEditingAction, "name": "Save and Finish Editing", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "dialogService", @@ -207,7 +207,7 @@ define([ "category": "save", "implementation": SaveAction, "name": "Save and Continue Editing", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "dialogService", @@ -219,7 +219,7 @@ define([ "category": "save", "implementation": SaveAsAction, "name": "Save As...", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "$injector", @@ -237,7 +237,7 @@ define([ // Because we use the name as label for edit buttons and mct-control buttons need // the label to be set to undefined in order to not apply the labeled CSS rule. "name": undefined, - "cssclass": "icon-x no-label", + "cssClass": "icon-x no-label", "description": "Discard changes made to these objects.", "depends": [] } diff --git a/platform/commonUI/edit/res/templates/create/create-menu.html b/platform/commonUI/edit/res/templates/create/create-menu.html index 64c05e9d38..32ba58431f 100644 --- a/platform/commonUI/edit/res/templates/create/create-menu.html +++ b/platform/commonUI/edit/res/templates/create/create-menu.html @@ -25,14 +25,14 @@
  • + class="menu-item-a {{ createAction.getMetadata().cssClass }}"> {{createAction.getMetadata().name}}