From 8b390e7fb9bc8dc241d57477acd84ee89ec4469f Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 20 Apr 2016 19:31:54 -0700 Subject: [PATCH] Revert "[Tables] Fix to correct sorting in realtime tables" --- docs/src/guide/index.md | 68 +----- platform/features/table/bundle.js | 24 +-- platform/features/table/res/sass/table.scss | 50 ----- .../table/res/templates/mct-table.html | 43 ++-- .../table/res/templates/rt-table.html | 2 +- .../{historical-table.html => table.html} | 2 +- .../features/table/src/TableConfiguration.js | 22 +- .../controllers/HistoricalTableController.js | 70 ------- .../src/controllers/MCTTableController.js | 195 ++++++++++-------- ...oller.js => RTTelemetryTableController.js} | 73 ++++--- .../src/controllers/TableOptionsController.js | 27 +-- .../controllers/TelemetryTableController.js | 94 +++++---- .../features/table/src/directives/MCTTable.js | 45 ---- .../table/test/TableConfigurationSpec.js | 8 +- .../controllers/MCTTableControllerSpec.js | 97 ++------- ...c.js => RTTelemetryTableControllerSpec.js} | 13 +- .../controllers/TableOptionsControllerSpec.js | 22 +- ...pec.js => TelemetryTableControllerSpec.js} | 17 +- 18 files changed, 293 insertions(+), 579 deletions(-) delete mode 100644 platform/features/table/res/sass/table.scss rename platform/features/table/res/templates/{historical-table.html => table.html} (71%) delete mode 100644 platform/features/table/src/controllers/HistoricalTableController.js rename platform/features/table/src/controllers/{RealtimeTableController.js => RTTelemetryTableController.js} (59%) rename platform/features/table/test/controllers/{RealtimeTableControllerSpec.js => RTTelemetryTableControllerSpec.js} (94%) rename platform/features/table/test/controllers/{HistoricalTableControllerSpec.js => TelemetryTableControllerSpec.js} (94%) diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 7016462a5a..93cb95bb7a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -6,13 +6,12 @@ Victor Woeltjen September 23, 2015 Document Version 1.1 -Date | Version | Summary of Changes | Author -------------------- | --------- | ------------------------- | --------------- -April 29, 2015 | 0 | Initial Draft | Victor Woeltjen -May 12, 2015 | 0.1 | | Victor Woeltjen -June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen -October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry -April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry +Date | Version | Summary of Changes | Author +------------------- | --------- | ----------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry # Introduction The purpose of this guide is to familiarize software developers with the Open @@ -1601,61 +1600,6 @@ there are items . ] } -## Table - -The `mct-table` directive provides a generic table component, with optional -sorting and filtering capabilities. The table can be pre-populated with data -by setting the `rows` parameter, and it can be updated in real-time using the -`add:row` and `remove:row` broadcast events. The table will expand to occupy -100% of the size of its containing element. The table is highly optimized for -very large data sets. - -### Events - -The table supports two events for notifying that the rows have changed. For -performance reasons, the table does not monitor the content of `rows` -constantly. - -* `add:row`: A `$broadcast` event that will notify the table that a new row -has been added to the table. - -eg. The code below adds a new row, and alerts the table using the `add:row` -event. Sorting and filtering will be applied automatically by the table component. - -``` -$scope.rows.push(newRow); -$scope.$broadcast('add:row', $scope.rows.length-1); -``` - -* `remove:row`: A `$broadcast` event that will notify the table that a row -should be removed from the table. - -eg. The code below removes a row from the rows array, and then alerts the table -to its removal. - -``` -$scope.rows.slice(5, 1); -$scope.$broadcast('remove:row', 5); -``` - -### Parameters - -* `headers`: An array of string values which will constitute the column titles - that appear at the top of the table. Corresponding values are specified in - the rows using the header title provided here. -* `rows`: An array of objects containing row values. Each element in the -array must be an associative array, where the key corresponds to a column header. -* `enableFilter`: A boolean that if true, will enable searching and result -filtering. When enabled, each column will have a text input field that can be -used to filter the table rows in real time. -* `enableSort`: A boolean determining whether rows can be sorted. If true, -sorting will be enabled allowing sorting by clicking on column headers. Only -one column may be sorted at a time. -* `autoScroll`: A boolean value that if true, will cause the table to automatically -scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually -by scrolling away from the bottom of the table, and can also be enabled manually -by scrolling to the bottom of the table rows. - # Services The Open MCT Web platform provides a variety of services which can be retrieved diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 0220b7dc6d..77ead67c04 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -23,16 +23,16 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RealtimeTableController", - "./src/controllers/HistoricalTableController", + "./src/controllers/RTTelemetryTableController", + "./src/controllers/TelemetryTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "legacyRegistry" ], function ( MCTTable, - RealtimeTableController, - HistoricalTableController, + RTTelemetryTableController, + TelemetryTableController, TableOptionsController, Region, InspectorRegion, @@ -109,13 +109,13 @@ define([ ], "controllers": [ { - "key": "HistoricalTableController", - "implementation": HistoricalTableController, + "key": "TelemetryTableController", + "implementation": TelemetryTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { - "key": "RealtimeTableController", - "implementation": RealtimeTableController, + "key": "RTTelemetryTableController", + "implementation": RTTelemetryTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { @@ -130,7 +130,7 @@ define([ "name": "Historical Table", "key": "table", "glyph": "\ue604", - "templateUrl": "templates/historical-table.html", + "templateUrl": "templates/table.html", "needs": [ "telemetry" ], @@ -161,12 +161,6 @@ define([ "key": "table-options-edit", "templateUrl": "templates/table-options-edit.html" } - ], - "stylesheets": [ - { - "stylesheetUrl": "css/table.css", - "priority": "mandatory" - } ] } }); diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss deleted file mode 100644 index a79cfac4c6..0000000000 --- a/platform/features/table/res/sass/table.scss +++ /dev/null @@ -1,50 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT Web includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -.sizing-table { - min-width: 100%; - z-index: -1; - visibility: hidden; - position: absolute; - - //Add some padding to allow for decorations such as limits indicator - td { - padding-right: 15px; - padding-left: 10px; - white-space: nowrap; - } -} -.mct-table { - table-layout: fixed; - th { - box-sizing: border-box; - } - tbody { - tr { - position: absolute; - } - td { - white-space: nowrap; - overflow: hidden; - box-sizing: border-box; - } - } -} \ No newline at end of file diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 7a18388455..5997376587 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -1,25 +1,22 @@ -
- - - - - - - -
{{header}}
- {{sizingRow[header].text}} -
- +
+ }"> - +
@@ -42,15 +41,21 @@
{{ visibleRow.contents[header].text }} diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html index d35015c96c..326c5b847b 100644 --- a/platform/features/table/res/templates/rt-table.html +++ b/platform/features/table/res/templates/rt-table.html @@ -1,4 +1,4 @@ -
+
+
0) { this.insertSorted(this.$scope.displayRows, row); - - //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)); } + + this.$timeout(this.setElementSizes.bind(this)) + .then(this.scrollToBottom.bind(this)); }; /** - * Handles a row remove event. Rows can be removed as needed using the - * `remove:row` broadcast event. + * Handles a row add event. Rows can be added as needed using the + * `addRow` broadcast event. * @private */ MCTTableController.prototype.removeRow = function (event, rowIndex) { @@ -232,7 +225,7 @@ define( * enabled, reset filters. If sorting is enabled, reset * sorting. */ - MCTTableController.prototype.setHeaders = function (newHeaders) { + MCTTableController.prototype.updateHeaders = function (newHeaders) { if (!newHeaders){ return; } @@ -248,7 +241,7 @@ define( this.$scope.sortColumn = undefined; this.$scope.sortDirection = undefined; } - this.setRows(this.$scope.rows); + this.updateRows(this.$scope.rows); }; /** @@ -256,12 +249,13 @@ define( * for individual rows. */ MCTTableController.prototype.setElementSizes = function () { - var thead = this.thead, - tbody = this.tbody, + var self = this, + thead = this.element.find('thead'), + tbody = this.element.find('tbody'), firstRow = tbody.find('tr'), column = firstRow.find('td'), headerHeight = thead.prop('offsetHeight'), - rowHeight = firstRow.prop('offsetHeight'), + rowHeight = 20, columnWidth, tableWidth = 0, overallHeight = headerHeight + (rowHeight * @@ -278,12 +272,15 @@ define( this.$scope.headerHeight = headerHeight; this.$scope.rowHeight = rowHeight; this.$scope.totalHeight = overallHeight; + this.setVisibleRows(); if (tableWidth > 0) { this.$scope.totalWidth = tableWidth + 'px'; } else { this.$scope.totalWidth = 'none'; } + + this.$scope.overrideRowPositioning = true; }; /** @@ -295,14 +292,21 @@ define( sortKey = this.$scope.sortColumn; function binarySearch(searchArray, searchElement, min, max){ - var sampleAt = Math.floor((max - min) / 2) + min; - + var sampleAt = Math.floor((max - min) / 2) + min, + valA, + valB; if (max < min) { return min; // Element is not in array, min gives direction } - switch(self.sortComparator(searchElement[sortKey].text, - searchArray[sampleAt][sortKey].text)) { + valA = isNaN(searchElement[sortKey].text) ? + searchElement[sortKey].text : + parseFloat(searchElement[sortKey].text); + valB = isNaN(searchArray[sampleAt][sortKey].text) ? + searchArray[sampleAt][sortKey].text : + parseFloat(searchArray[sampleAt][sortKey].text); + + switch(self.sortComparator(valA, valB)) { case -1: return binarySearch(searchArray, searchElement, min, sampleAt - 1); @@ -340,34 +344,8 @@ define( */ MCTTableController.prototype.sortComparator = function (a, b) { var result = 0, - sortDirectionMultiplier, - numberA, - numberB; - /** - * Given a value, if it is a number, or a string representation of a - * number, then return a number representation. Otherwise, return - * the original value. It's a little more robust than using just - * Number() or parseFloat, or isNaN in isolation, all of which are - * fairly inconsistent in their results. - * @param value The value to return as a number. - * @returns {*} The value cast to a Number, or the original value if - * a Number representation is not possible. - */ - function toNumber (value){ - var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; - return val; - } + sortDirectionMultiplier; - numberA = toNumber(a); - numberB = toNumber(b); - - //If they're both numbers, then compare them as numbers - if (typeof numberA === "number" && typeof numberB === "number") { - a = numberA; - b = numberB; - } - - //If they're both strings, then ignore case if (typeof a === "string" && typeof b === "string") { a = a.toLowerCase(); b = b.toLowerCase(); @@ -404,7 +382,15 @@ define( } return rowsToSort.sort(function (a, b) { - return self.sortComparator(a[sortKey].text, b[sortKey].text); + //If the values to compare can be compared as + // numbers, do so. String comparison of number + // values can cause inconsistencies + var valA = isNaN(a[sortKey].text) ? a[sortKey].text : + parseFloat(a[sortKey].text), + valB = isNaN(b[sortKey].text) ? b[sortKey].text : + parseFloat(b[sortKey].text); + + return self.sortComparator(valA, valB); }); }; @@ -414,48 +400,74 @@ define( * pre-calculate optimal column sizes without having to render * every row. */ - MCTTableController.prototype.buildLargestRow = function (rows) { - var largestRow = rows.reduce(function (prevLargest, row) { + MCTTableController.prototype.findLargestRow = function (rows) { + var largestRow = rows.reduce(function (largestRow, row) { Object.keys(row).forEach(function (key) { - var currentColumn, - currentColumnLength, - largestColumn, - largestColumnLength; - if (row[key]){ - currentColumn = (row[key]).text; + var currentColumn = row[key].text, currentColumnLength = (currentColumn && currentColumn.length) ? currentColumn.length : - currentColumn; - largestColumn = prevLargest[key] ? prevLargest[key].text : ""; - largestColumnLength = largestColumn.length; + currentColumn, + largestColumn = largestRow[key].text, + largestColumnLength = + (largestColumn && largestColumn.length) ? + largestColumn.length : + largestColumn; - if (currentColumnLength > largestColumnLength) { - prevLargest[key] = JSON.parse(JSON.stringify(row[key])); - } + if (currentColumnLength > largestColumnLength) { + largestRow[key] = JSON.parse(JSON.stringify(row[key])); } }); - return prevLargest; + return largestRow; }, JSON.parse(JSON.stringify(rows[0] || {}))); + + largestRow = JSON.parse(JSON.stringify(largestRow)); + + // Pad with characters to accomodate variable-width fonts, + // and remove characters that would allow word-wrapping. + Object.keys(largestRow).forEach(function (key) { + var padCharacters, + i; + + largestRow[key].text = String(largestRow[key].text); + padCharacters = largestRow[key].text.length / 10; + for (i = 0; i < padCharacters; i++) { + largestRow[key].text = largestRow[key].text + 'W'; + } + largestRow[key].text = largestRow[key].text + .replace(/[ \-_]/g, 'W'); + }); return largestRow; }; /** - * Calculates the widest row in the table, and if necessary, resizes - * the table accordingly - * - * @param rows the rows on which to resize - * @returns {Promise} a promise that will resolve when resizing has - * occurred. + * Calculates the widest row in the table, pads that row, and adds + * it to the table. Allows the table to size itself, then uses this + * as basis for column dimensions. * @private */ - MCTTableController.prototype.resize = function (rows) { - this.$scope.sizingRow = this.buildLargestRow(rows); - return this.$timeout(this.setElementSizes.bind(this)); + MCTTableController.prototype.resize = function (){ + var largestRow = this.findLargestRow(this.$scope.displayRows), + self = this; + this.$scope.visibleRows = [ + { + rowIndex: 0, + offsetY: undefined, + contents: largestRow + } + ]; + + //Wait a timeout to allow digest of previous change to visible + // rows to happen. + this.$timeout(function () { + //Remove temporary padding row used for setting column widths + self.$scope.visibleRows = []; + self.setElementSizes(); + }); }; /** - * @private + * @priate */ MCTTableController.prototype.filterAndSort = function (rows) { var displayRows = rows; @@ -466,21 +478,26 @@ define( if (this.$scope.enableSort) { displayRows = this.sortRows(displayRows.slice(0)); } - return displayRows; + this.$scope.displayRows = displayRows; }; /** * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.setRows = function (newRows) { + MCTTableController.prototype.updateRows = function (newRows) { + //Reset visible rows because new row data available. + this.$scope.visibleRows = []; + + this.$scope.overrideRowPositioning = false; + //Nothing to show because no columns visible - if (!this.$scope.displayHeaders || !newRows) { + if (!this.$scope.displayHeaders) { return; } - this.$scope.displayRows = this.filterAndSort(newRows || []); - this.resize(newRows).then(this.setVisibleRows.bind(this)); + this.filterAndSort(newRows || []); + this.resize(); }; /** diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RTTelemetryTableController.js similarity index 59% rename from platform/features/table/src/controllers/RealtimeTableController.js rename to platform/features/table/src/controllers/RTTelemetryTableController.js index 3f983207bc..8a61d61b5e 100644 --- a/platform/features/table/src/controllers/RealtimeTableController.js +++ b/platform/features/table/src/controllers/RTTelemetryTableController.js @@ -37,7 +37,7 @@ define( * @param telemetryFormatter * @constructor */ - function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) { + function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) { TableController.call(this, $scope, telemetryHandler, telemetryFormatter); $scope.autoScroll = false; @@ -66,35 +66,58 @@ define( }); } - RealtimeTableController.prototype = Object.create(TableController.prototype); + RTTelemetryTableController.prototype = Object.create(TableController.prototype); /** - * Overrides method on TelemetryTableController providing handling - * for realtime data. + Override the subscribe function defined on the parent controller in + order to handle realtime telemetry instead of historical. */ - 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); - } + RTTelemetryTableController.prototype.subscribe = function () { + var self = this; + self.$scope.rows = undefined; + (this.subscriptions || []).forEach(function (unsubscribe){ + unsubscribe(); }); + + if (this.handle) { + this.handle.unsubscribe(); + } + + function updateData(){ + var datum, + row; + self.handle.getTelemetryObjects().forEach(function (telemetryObject){ + datum = self.handle.getDatum(telemetryObject); + if (datum) { + row = self.table.getRowValues(telemetryObject, datum); + if (!self.$scope.rows){ + self.$scope.rows = [row]; + self.$scope.$digest(); + } else { + self.$scope.rows.push(row); + + 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.handle = this.$scope.domainObject && this.telemetryHandler.handle( + this.$scope.domainObject, + updateData, + true // Lossless + ); + + this.setup(); }; - return RealtimeTableController; + return RTTelemetryTableController; } ); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js index a28411e7d0..c3b479073c 100644 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -51,29 +51,13 @@ define( this.$scope = $scope; this.domainObject = $scope.domainObject; - this.listeners = []; $scope.columnsForm = {}; - function unlisten() { - self.listeners.forEach(function (listener) { - listener(); - }); - } - - $scope.$watch('domainObject', function(domainObject) { - unlisten(); - self.populateForm(domainObject.getModel()); - - self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) { - self.populateForm(model); - })); + this.domainObject.getCapability('mutation').listen(function (model) { + self.populateForm(model); }); - /** - * Maintain a configuration object on scope that stores column - * configuration. On change, synchronize with object model. - */ $scope.$watchCollection('configuration.table.columns', function (columns){ if (columns){ self.domainObject.useCapability('mutation', function (model) { @@ -83,11 +67,6 @@ define( } }); - /** - * Destroy all mutation listeners - */ - $scope.$on('$destroy', unlisten); - } TableOptionsController.prototype.populateForm = function (model) { @@ -107,7 +86,7 @@ define( 'key': key }); }); - this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {})); + this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration)); }; return TableOptionsController; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 36f54b1ac6..e579c5eeb8 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -52,15 +52,19 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; + //this.pending = false; this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, telemetryFormatter); this.changeListeners = []; - $scope.rows = []; + $scope.rows = undefined; // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function(){ + this.$scope.$watch('domainObject', function(domainObject){ + if (!domainObject) + return; + self.subscribe(); self.registerChangeListeners(); }); @@ -69,24 +73,16 @@ define( this.$scope.$on("$destroy", this.destroy.bind(this)); } - /** - * @private - */ - 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 * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.unregisterChangeListeners(); - + this.changeListeners.forEach(function (listener) { + return listener && listener(); + }); + this.changeListeners = []; // When composition changes, re-subscribe to the various // telemetry subscriptions this.changeListeners.push(this.$scope.$watchCollection( @@ -107,37 +103,25 @@ define( } }; - /** - * 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 change default behaviour (which is to retrieve historical telemetry only). */ TelemetryTableController.prototype.subscribe = function () { + var self = this; + if (this.handle) { this.handle.unsubscribe(); } + //Noop because not supporting realtime data right now + function noop(){ + } + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( this.$scope.domainObject, - this.addRealtimeData.bind(this), + noop, true // Lossless ); @@ -146,6 +130,28 @@ define( this.setup(); }; + /** + * Populates historical data on scope when it becomes available + * @private + */ + TelemetryTableController.prototype.addHistoricalData = function () { + var rowData = [], + self = this; + + this.handle.getTelemetryObjects().forEach(function (telemetryObject){ + var series = self.handle.getSeries(telemetryObject) || {}, + pointCount = series.getPointCount ? series.getPointCount() : 0, + i = 0; + + for (; i < pointCount; i++) { + rowData.push(self.table.getRowValues(telemetryObject, + self.handle.makeDatum(telemetryObject, series, i))); + } + }); + + this.$scope.rows = rowData; + }; + /** * Setup table columns based on domain object metadata */ @@ -156,9 +162,7 @@ define( if (handle) { handle.promiseTelemetryObjects().then(function () { - self.$scope.headers = []; - self.$scope.rows = []; - table.populateColumns(handle.getMetadata()); + table.buildColumns(handle.getMetadata()); self.filterColumns(); @@ -172,14 +176,26 @@ define( } }; + /** + * @private + * @param object The object for which data is available (table may + * be composed of multiple objects) + * @param datum The data received from the telemetry source + */ + TelemetryTableController.prototype.updateRows = function (object, datum) { + this.$scope.rows.push(this.table.getRowValues(object, datum)); + }; + /** * When column configuration changes, update the visible headers * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function () { - var columnConfig = this.table.buildColumnConfiguration(); - + TelemetryTableController.prototype.filterColumns = function (columnConfig) { + if (!columnConfig){ + columnConfig = this.table.getColumnConfiguration(); + this.table.saveColumnConfiguration(columnConfig); + } //Populate headers with visible columns (determined by configuration) this.$scope.headers = Object.keys(columnConfig).filter(function (column) { return columnConfig[column]; diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index 2d61669a2e..575e830395 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -12,51 +12,6 @@ define( * Defines a generic 'Table' component. The table can be populated * en-masse by setting the rows attribute, or rows can be added as * needed via a broadcast 'addRow' event. - * - * This directive accepts parameters specifying header and row - * content, as well as some additional options. - * - * Two broadcast events for notifying the table that the rows have - * changed. For performance reasons, the table does not monitor the - * content of `rows` constantly. - * - 'add:row': A $broadcast event that will notify the table that - * a new row has been added to the table. - * eg. - *

-         * $scope.rows.push(newRow);
-         * $scope.$broadcast('add:row', $scope.rows.length-1);
-         * 
- * The code above adds a new row, and alerts the table using the - * add:row event. Sorting and filtering will be applied - * automatically by the table component. - * - * - 'remove:row': A $broadcast event that will notify the table that a - * row should be removed from the table. - * eg. - *

-         * $scope.rows.slice(5, 1);
-         * $scope.$broadcast('remove:row', 5);
-         * 
- * The code above removes a row from the rows array, and then alerts - * the table to its removal. - * - * @memberof platform/features/table - * @param {string[]} headers The column titles to appear at the top - * of the table. Corresponding values are specified in the rows - * using the header title provided here. - * @param {Object[]} rows The row content. Each row is an object - * with key-value pairs where the key corresponds to a header - * specified in the headers parameter. - * @param {boolean} enableFilter If true, values will be searchable - * and results filtered - * @param {boolean} enableSort If true, sorting will be enabled - * allowing sorting by clicking on column headers - * @param {boolean} autoScroll If true, table will automatically - * scroll to the bottom as new data arrives. Auto-scroll can be - * disengaged manually by scrolling away from the bottom of the - * table, and can also be enabled manually by scrolling to the bottom of - * the table rows. - * * @constructor */ function MCTTable($timeout) { diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index bfc6f50edb..86a18aee5a 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -116,10 +116,10 @@ define( }]; beforeEach(function() { - table.populateColumns(metadata); + table.buildColumns(metadata); }); - it("populates columns", function() { + it("populates the columns attribute", function() { expect(table.columns.length).toBe(5); }); @@ -141,7 +141,7 @@ define( it("Provides a default configuration with all columns" + " visible", function() { - var configuration = table.buildColumnConfiguration(); + var configuration = table.getColumnConfiguration(); expect(configuration).toBeDefined(); expect(Object.keys(configuration).every(function(key){ @@ -160,7 +160,7 @@ define( }; mockModel.configuration = modelConfig; - tableConfig = table.buildColumnConfiguration(); + tableConfig = table.getColumnConfiguration(); expect(tableConfig).toBeDefined(); expect(tableConfig['Range 1']).toBe(false); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 175d8fed0a..5e38c7e651 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -58,18 +58,15 @@ define( mockElement = jasmine.createSpyObj('element', [ 'find', - 'prop', 'on' ]); mockElement.find.andReturn(mockElement); - mockElement.prop.andReturn(0); mockScope.displayHeaders = true; mockTimeout = jasmine.createSpy('$timeout'); mockTimeout.andReturn(promise(undefined)); controller = new MCTTableController(mockScope, mockTimeout, mockElement); - spyOn(controller, 'setVisibleRows'); }); it('Reacts to changes to filters, headers, and rows', function() { @@ -118,7 +115,7 @@ define( }); it('Sets rows on scope when rows change', function() { - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows).toEqual(testRows); }); @@ -130,7 +127,7 @@ define( 'col2': {'text': 'ghi'}, 'col3': {'text': 'row3 col3'} }; - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); addRowFunc(undefined, 3); @@ -139,8 +136,10 @@ define( it('Supports removing rows individually', function() { var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1]; - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); + spyOn(controller, 'setVisibleRows'); + //controller.setVisibleRows.andReturn(undefined); removeRowFunc(undefined, 2); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); @@ -180,54 +179,7 @@ define( expect(sortedRows[2].col2.text).toEqual('abc'); }); - it('correctly sorts rows of differing types', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'desc'; - - testRows.push({ - 'col1': {'text': 'row4 col1'}, - 'col2': {'text': '123'}, - 'col3': {'text': 'row4 col3'} - }); - testRows.push({ - 'col1': {'text': 'row5 col1'}, - 'col2': {'text': '456'}, - 'col3': {'text': 'row5 col3'} - }); - testRows.push({ - 'col1': {'text': 'row5 col1'}, - 'col2': {'text': ''}, - 'col3': {'text': 'row5 col3'} - }); - - sortedRows = controller.sortRows(testRows); - expect(sortedRows[0].col2.text).toEqual('ghi'); - expect(sortedRows[1].col2.text).toEqual('def'); - expect(sortedRows[2].col2.text).toEqual('abc'); - - expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456'); - expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123'); - expect(sortedRows[sortedRows.length-1].col2.text).toEqual(''); - }); - - describe('The sort comparator', function () { - it('Correctly sorts different data types', function () { - var val1 = "", - val2 = "1", - val3 = "2016-04-05 18:41:30.713Z", - val4 = "1.1", - val5 = "8.945520958175627e-13"; - mockScope.sortDirection = "asc"; - - expect(controller.sortComparator(val1, val2)).toEqual(-1); - expect(controller.sortComparator(val3, val1)).toEqual(1); - expect(controller.sortComparator(val3, val2)).toEqual(1); - expect(controller.sortComparator(val4, val2)).toEqual(1); - expect(controller.sortComparator(val2, val5)).toEqual(1); - }); - }); - - describe('Adding new rows', function () { + describe('Adding new rows', function() { var row4, row5, row6; @@ -258,20 +210,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(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.newRow(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.newRow(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.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -287,18 +239,18 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(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.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); it('Adds new rows at the correct sort position when' + - ' not sorted ', function () { + ' not sorted ', function() { mockScope.sortColumn = undefined; mockScope.sortDirection = undefined; mockScope.filters = {}; @@ -306,33 +258,14 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(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.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); - it('Resizes columns if length of any columns in new' + - ' row exceeds corresponding existing column', function() { - var row7 = { - 'col1': {'text': 'row6 col1'}, - 'col2': {'text': 'some longer string'}, - 'col3': {'text': 'row6 col3'} - }; - - mockScope.sortColumn = undefined; - mockScope.sortDirection = undefined; - mockScope.filters = {}; - - mockScope.displayRows = testRows.slice(0); - - mockScope.rows.push(row7); - controller.addRow(undefined, mockScope.rows.length-1); - 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/RTTelemetryTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/RealtimeTableControllerSpec.js rename to platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js index e0b6978d88..59911d1771 100644 --- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js +++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/RealtimeTableController" + "../../src/controllers/RTTelemetryTableController" ], function (TableController) { "use strict"; @@ -77,14 +77,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'populateColumns', - 'buildColumnConfiguration', + 'buildColumns', + 'getColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); + mockTable.getColumnConfiguration.andReturn(mockConfiguration); mockTable.getRowValues.andReturn(mockTableRow); mockDomainObject= jasmine.createSpyObj('domainObject', [ @@ -107,16 +107,13 @@ define( 'unsubscribe', 'getDatum', 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'request' + 'getTelemetryObjects' ]); - // 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)); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ 'handle' diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js index fd6d8b43fe..9de96b5f52 100644 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -47,36 +47,18 @@ define( 'listen' ]); mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'getModel' + 'getCapability' ]); mockDomainObject.getCapability.andReturn(mockCapability); - mockDomainObject.getModel.andReturn({}); - mockScope = jasmine.createSpyObj('scope', [ - '$watchCollection', - '$watch', - '$on' + '$watchCollection' ]); mockScope.domainObject = mockDomainObject; controller = new TableOptionsController(mockScope); }); - it('Listens for changing domain object', function() { - expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function)); - }); - - it('On destruction of controller, destroys listeners', function() { - var unlistenFunc = jasmine.createSpy("unlisten"); - controller.listeners.push(unlistenFunc); - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); - mockScope.$on.mostRecentCall.args[1](); - expect(unlistenFunc).toHaveBeenCalled(); - }); - it('Registers a listener for mutation events on the object', function() { - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(mockCapability.listen).toHaveBeenCalled(); }); diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/HistoricalTableControllerSpec.js rename to platform/features/table/test/controllers/TelemetryTableControllerSpec.js index f000529467..03f62f11e3 100644 --- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/HistoricalTableController" + "../../src/controllers/TelemetryTableController" ], function (TableController) { "use strict"; @@ -73,14 +73,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'populateColumns', - 'buildColumnConfiguration', + 'buildColumns', + 'getColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); + mockTable.getColumnConfiguration.andReturn(mockConfiguration); mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', @@ -126,18 +126,21 @@ define( expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); }); - describe('makes use of the table', function () { + describe('the controller makes use of the table', function () { it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); - expect(mockTable.populateColumns).toHaveBeenCalled(); + expect(mockTable.buildColumns).toHaveBeenCalled(); }); it('to create column configuration, which is written to the' + ' object model', function () { + var mockModel = {}; + controller.setup(); - expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.getColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.saveColumnConfiguration).toHaveBeenCalled(); }); });