From ffa497f22f996e1fc7feabaa5e91f18c9c0a5792 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 26 Feb 2016 11:09:51 -0800 Subject: [PATCH] [Table] #670 Added new view type 'table' --- main.js | 1 + platform/features/table/bundle.js | 132 +++++++ .../table/res/templates/mct-data-table.html | 67 ++++ .../res/templates/table-options-edit.html | 30 ++ .../features/table/res/templates/table.html | 8 + platform/features/table/src/DomainColumn.js | 64 ++++ platform/features/table/src/NameColumn.js | 54 +++ platform/features/table/src/RangeColumn.js | 67 ++++ platform/features/table/src/Table.js | 163 ++++++++ .../src/controllers/MCTTableController.js | 354 ++++++++++++++++++ .../table/src/controllers/TableController.js | 214 +++++++++++ .../src/controllers/TableOptionsController.js | 94 +++++ .../features/table/src/directives/MCTTable.js | 24 ++ .../features/table/test/DomainColumnSpec.js | 84 +++++ .../features/table/test/NameColumnSpec.js | 58 +++ .../features/table/test/RangeColumnSpec.js | 76 ++++ platform/features/table/test/TableSpec.js | 197 ++++++++++ .../controllers/MCTTableControllerSpec.js | 155 ++++++++ .../test/controllers/TableControllerSpec.js | 224 +++++++++++ .../controllers/TableOptionsControllerSpec.js | 105 ++++++ 20 files changed, 2171 insertions(+) create mode 100644 platform/features/table/bundle.js create mode 100644 platform/features/table/res/templates/mct-data-table.html create mode 100644 platform/features/table/res/templates/table-options-edit.html create mode 100644 platform/features/table/res/templates/table.html create mode 100644 platform/features/table/src/DomainColumn.js create mode 100644 platform/features/table/src/NameColumn.js create mode 100644 platform/features/table/src/RangeColumn.js create mode 100644 platform/features/table/src/Table.js create mode 100644 platform/features/table/src/controllers/MCTTableController.js create mode 100644 platform/features/table/src/controllers/TableController.js create mode 100644 platform/features/table/src/controllers/TableOptionsController.js create mode 100644 platform/features/table/src/directives/MCTTable.js create mode 100644 platform/features/table/test/DomainColumnSpec.js create mode 100644 platform/features/table/test/NameColumnSpec.js create mode 100644 platform/features/table/test/RangeColumnSpec.js create mode 100644 platform/features/table/test/TableSpec.js create mode 100644 platform/features/table/test/controllers/MCTTableControllerSpec.js create mode 100644 platform/features/table/test/controllers/TableControllerSpec.js create mode 100644 platform/features/table/test/controllers/TableOptionsControllerSpec.js diff --git a/main.js b/main.js index ab911921ec..bf7cbbd90d 100644 --- a/main.js +++ b/main.js @@ -74,6 +74,7 @@ define([ './platform/features/plot/bundle', './platform/features/scrolling/bundle', './platform/features/timeline/bundle', + './platform/features/table/bundle', './platform/forms/bundle', './platform/identity/bundle', './platform/persistence/aggregator/bundle', diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js new file mode 100644 index 0000000000..b598ec1200 --- /dev/null +++ b/platform/features/table/bundle.js @@ -0,0 +1,132 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define([ + "./src/directives/MCTTable", + "./src/controllers/TableController", + "./src/controllers/TableOptionsController", + "./src/controllers/MCTTableController", + '../../commonUI/regions/src/Region', + '../../commonUI/browse/src/InspectorRegion', + "legacyRegistry" +], function ( + MCTTable, + TableController, + TableOptionsController, + MCTTableController, + Region, + InspectorRegion, + legacyRegistry +) { + "use strict"; + /** + * Two region parts are defined here. One that appears only in browse + * mode, and one that appears only in edit mode. For not they both point + * to the same representation, but a different key could be used here to + * include a customized representation for edit mode. + */ + var tableInspector = new InspectorRegion(), + tableOptionsEditRegion = new Region({ + name: "table-options", + title: "Table Options", + modes: ['edit'], + content: { + key: "table-options-edit" + } + }); + tableInspector.addRegion(tableOptionsEditRegion); + + legacyRegistry.register("platform/features/table", { + "extensions": { + "types": [ + { + "key": "table", + "name": "Table", + "glyph": "\ue605", + "description": "A table for displaying telemetry data", + "features": "creation", + "delegates": [ + "telemetry" + ], + "inspector": tableInspector, + "contains": [ + { + "has": "telemetry" + } + ], + "model": { + "composition": [] + }, + "views": [ + "table" + ] + } + ], + "controllers": [ + { + "key": "TableController", + "implementation": TableController, + "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] + }, + { + "key": "TableOptionsController", + "implementation": TableOptionsController, + "depends": ["$scope"] + }, + { + "key": "MCTTableController", + "implementation": MCTTableController, + "depends": ["$scope", "$timeout", "$element"] + } + + ], + "views": [ + { + "name": "Table", + "key": "table", + "glyph": "\ue605", + "templateUrl": "templates/table.html", + "needs": [ + "telemetry" + ], + "delegation": true, + "editable": true + } + ], + "directives": [ + { + "key": "mctTable", + "implementation": MCTTable, + "depends": ["$timeout"] + } + ], + "representations": [ + { + "key": "table-options-edit", + "templateUrl": "templates/table-options-edit.html" + } + ] + } + }); + +}); diff --git a/platform/features/table/res/templates/mct-data-table.html b/platform/features/table/res/templates/mct-data-table.html new file mode 100644 index 0000000000..42753a8a7b --- /dev/null +++ b/platform/features/table/res/templates/mct-data-table.html @@ -0,0 +1,67 @@ +
+ + + + + + + + + + + + + + + + +
+ {{ header }} +
+ +
+ {{ visibleRow.contents[header].text }} +
+
diff --git a/platform/features/table/res/templates/table-options-edit.html b/platform/features/table/res/templates/table-options-edit.html new file mode 100644 index 0000000000..39884b18b1 --- /dev/null +++ b/platform/features/table/res/templates/table-options-edit.html @@ -0,0 +1,30 @@ + +
+ Display + + +
\ No newline at end of file diff --git a/platform/features/table/res/templates/table.html b/platform/features/table/res/templates/table.html new file mode 100644 index 0000000000..12faebe40b --- /dev/null +++ b/platform/features/table/res/templates/table.html @@ -0,0 +1,8 @@ +
+ + +
\ No newline at end of file diff --git a/platform/features/table/src/DomainColumn.js b/platform/features/table/src/DomainColumn.js new file mode 100644 index 0000000000..1dbe0ab73d --- /dev/null +++ b/platform/features/table/src/DomainColumn.js @@ -0,0 +1,64 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,moment*/ + +/** + * Module defining DomainColumn. + */ +define( + [], + function () { + "use strict"; + + /** + * A column which will report telemetry domain values + * (typically, timestamps.) Used by the ScrollingListController. + * + * @memberof platform/features/table + * @constructor + * @param domainMetadata an object with the machine- and human- + * readable names for this domain (in `key` and `name` + * fields, respectively.) + * @param {TelemetryFormatter} telemetryFormatter the telemetry + * formatting service, for making values human-readable. + */ + function DomainColumn(domainMetadata, telemetryFormatter) { + this.domainMetadata = domainMetadata; + this.telemetryFormatter = telemetryFormatter; + } + + DomainColumn.prototype.getTitle = function () { + return this.domainMetadata.name; + }; + + DomainColumn.prototype.getValue = function (domainObject, datum) { + return { + text: this.telemetryFormatter.formatDomainValue( + datum[this.domainMetadata.key], + this.domainMetadata.format + ) + }; + }; + + return DomainColumn; + } +); diff --git a/platform/features/table/src/NameColumn.js b/platform/features/table/src/NameColumn.js new file mode 100644 index 0000000000..72ace365e4 --- /dev/null +++ b/platform/features/table/src/NameColumn.js @@ -0,0 +1,54 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +/** + * Module defining NameColumn. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + "use strict"; + + /** + * A column which will report the name of the domain object + * which exposed specific telemetry values. + * + * @memberof platform/features/table + * @constructor + */ + function NameColumn() { + } + + NameColumn.prototype.getTitle = function () { + return "Name"; + }; + + NameColumn.prototype.getValue = function (domainObject) { + return { + text: domainObject.getModel().name + }; + }; + + return NameColumn; + } +); diff --git a/platform/features/table/src/RangeColumn.js b/platform/features/table/src/RangeColumn.js new file mode 100644 index 0000000000..0dfe964dc8 --- /dev/null +++ b/platform/features/table/src/RangeColumn.js @@ -0,0 +1,67 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +/** + * Module defining DomainColumn. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + "use strict"; + + /** + * A column which will report telemetry range values + * (typically, measurements.) Used by the ScrollingListController. + * + * @memberof platform/features/table + * @constructor + * @param rangeMetadata an object with the machine- and human- + * readable names for this range (in `key` and `name` + * fields, respectively.) + * @param {TelemetryFormatter} telemetryFormatter the telemetry + * formatting service, for making values human-readable. + */ + function RangeColumn(rangeMetadata, telemetryFormatter) { + this.rangeMetadata = rangeMetadata; + this.telemetryFormatter = telemetryFormatter; + } + + RangeColumn.prototype.getTitle = function () { + return this.rangeMetadata.name; + }; + + RangeColumn.prototype.getValue = function (domainObject, datum) { + var range = this.rangeMetadata.key, + limit = domainObject.getCapability('limit'), + value = isNaN(datum[range]) ? datum[range] : parseFloat(datum[range]), + alarm = limit && limit.evaluate(datum, range); + + return { + cssClass: alarm && alarm.cssClass, + text: typeof(value) === 'undefined' ? undefined : this.telemetryFormatter.formatRangeValue(value) + }; + }; + + return RangeColumn; + } +); diff --git a/platform/features/table/src/Table.js b/platform/features/table/src/Table.js new file mode 100644 index 0000000000..a12c9789b1 --- /dev/null +++ b/platform/features/table/src/Table.js @@ -0,0 +1,163 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,moment*/ + +define( + [ + './DomainColumn', + './RangeColumn', + './NameColumn' + ], + function (DomainColumn, RangeColumn, NameColumn) { + "use strict"; + + /** + * Class that manages table metadata, state, and contents. + * @memberof platform/features/table + * @param domainObject + * @constructor + */ + function Table(domainObject, telemetryFormatter) { + this.domainObject = domainObject; + this.columns = []; + this.telemetryFormatter = telemetryFormatter; + } + + /** + * Build column definitions based on supplied telemetry metadata + * @param metadata Metadata describing the domains and ranges available + * @returns {Table} This object + */ + Table.prototype.buildColumns = function(metadata) { + var self = this; + + this.columns = []; + + if (metadata) { + metadata.forEach(function (metadatum) { + //Push domains first + metadatum.domains.forEach(function (domainMetadata) { + self.addColumn(new DomainColumn(domainMetadata, self.telemetryFormatter)); + }); + metadatum.ranges.forEach(function (rangeMetadata) { + self.addColumn(new RangeColumn(rangeMetadata, self.telemetryFormatter)); + }); + }); + } + return this; + }; + + /** + * Add a column definition to this Table + * @param {RangeColumn | DomainColumn | NameColumn} column + * @param {Number} [index] Where the column should appear (will be + * affected by column filtering) + */ + Table.prototype.addColumn = function (column, index) { + if (typeof index === 'undefined') { + this.columns.push(column); + } else { + this.columns.splice(index, 0, column); + } + }; + + /** + * @private + * @param column + * @returns {*|string} + */ + Table.prototype.getColumnTitle = function (column) { + return column.getTitle(); + }; + + /** + * Get a simple list of column titles + * @returns {Array} The titles of the columns + */ + Table.prototype.getHeaders = function() { + var self = this; + return this.columns.map(function (column){ + return self.getColumnTitle(column); + }); + }; + + /** + * Retrieve and format values for a given telemetry datum. + * @param telemetryObject The object that the telemetry data is + * associated with + * @param datum The telemetry datum to retrieve values from + * @returns {Object} Key value pairs where the key is the column + * title, and the value is the formatted value from the provided datum. + */ + Table.prototype.getRowValues = function(telemetryObject, datum) { + var self = this; + return this.columns.reduce(function(rowObject, column){ + var columnTitle = self.getColumnTitle(column), + columnValue = column.getValue(telemetryObject, datum); + + if (columnValue !== undefined && columnValue.text === undefined){ + columnValue.text = ''; + } + // Don't replace something with nothing. + // This occurs when there are multiple columns with the + // column title + if (rowObject[columnTitle] === undefined || rowObject[columnTitle].text === undefined || rowObject[columnTitle].text.length === 0) { + rowObject[columnTitle] = columnValue; + } + return rowObject; + }, {}); + }; + + /** + * @private + */ + Table.prototype.defaultColumnConfiguration = function () { + return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {}; + }; + + /** + * As part of the process of building the table definition, extract + * configuration from column definitions. + * @returns {Object} A configuration object consisting of key-value + * pairs where the key is the column title, and the value is a + * boolean indicating whether the column should be shown. + */ + Table.prototype.getColumnConfiguration = function() { + var configuration = {}, + //Use existing persisted config, or default it + defaultConfig = this.defaultColumnConfiguration(); + + /** + * For each column header, define a configuration value + * specifying whether the column is visible or not. Default to + * existing (persisted) configuration if available + */ + this.getHeaders().forEach(function(columnTitle) { + configuration[columnTitle] = typeof defaultConfig[columnTitle] === 'undefined' ? true : defaultConfig[columnTitle]; + }); + + return configuration; + }; + + return Table; + } +); diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js new file mode 100644 index 0000000000..63a5aa5036 --- /dev/null +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -0,0 +1,354 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function MCTTableController($scope, $timeout, element) { + var self = this; + + this.$scope = $scope; + this.element = element; + this.$timeout = $timeout; + this.maxDisplayRows = 50; + + $scope.visibleRows = []; + $scope.overrideRowPositioning = false; + + /** + * Set default values for optional parameters on a given scope + */ + function setDefaults($scope) { + if (typeof $scope.enableFilter === 'undefined') { + $scope.enableFilter = true; + $scope.filters = {}; + } + if (typeof $scope.enableSort === 'undefined') { + $scope.enableSort = true; + $scope.sortColumn = undefined; + $scope.sortDirection = undefined; + } + } + + setDefaults($scope); + + element.find('div').on('scroll', this.onScroll.bind(this)); + + $scope.toggleSort = function (key) { + if (!$scope.enableSort) { + return; + } + if ($scope.sortColumn !== key) { + $scope.sortColumn = key; + $scope.sortDirection = 'asc'; + } else if ($scope.sortDirection === 'asc') { + $scope.sortDirection = 'desc'; + } else if ($scope.sortDirection === 'desc') { + $scope.sortColumn = undefined; + $scope.sortDirection = undefined; + } + self.updateRows($scope.rows); + }; + + $scope.$watchCollection('filters', function () { + self.updateRows(self.$scope.rows); + }); + $scope.$watchCollection('headers', this.updateHeaders.bind(this)); + $scope.$watchCollection('rows', this.updateRows.bind(this)); + } + + /** + * On scroll, calculate which rows indexes are visible and + * ensure that an equal number of rows are preloaded for + * scrolling in either direction. + */ + MCTTableController.prototype.onScroll = function (event) { + var self = this, + topScroll = event.target.scrollTop, + bottomScroll = topScroll + event.target.offsetHeight, + firstVisible, + lastVisible, + totalVisible, + numberOffscreen, + start, + end; + + if (this.$scope.displayRows.length < this.maxDisplayRows) { + return; + } + + if (topScroll < this.$scope.headerHeight) { + firstVisible = 0; + } else { + firstVisible = Math.floor( + (topScroll - this.$scope.headerHeight) / this.$scope.rowHeight + ); + } + lastVisible = Math.ceil( + (bottomScroll - this.$scope.headerHeight) / this.$scope.rowHeight + ); + + totalVisible = lastVisible - firstVisible; + numberOffscreen = this.maxDisplayRows - totalVisible; + start = firstVisible - Math.floor(numberOffscreen / 2); + end = lastVisible + Math.ceil(numberOffscreen / 2); + + if (start < 0) { + start = 0; + end = this.$scope.visibleRows.length - 1; + } else if (end >= this.$scope.displayRows.length) { + end = this.$scope.displayRows.length - 1; + start = end - this.maxDisplayRows + 1; + } + if (this.$scope.visibleRows[0].rowIndex === start && + this.$scope.visibleRows[this.$scope.visibleRows.length-1] + .rowIndex === end) { + + return; // don't update if no changes are required. + } + + this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) + .map(function(row, i) { + return { + rowIndex: start + i, + offsetY: ((start + i) * self.$scope.rowHeight) + + self.$scope.headerHeight, + contents: row + }; + }); + + this.$scope.$digest(); + }; + + /** + * Update table headers with new headers. If filtering is + * enabled, reset filters. If sorting is enabled, reset + * sorting. + */ + MCTTableController.prototype.updateHeaders = function (newHeaders) { + if (!newHeaders){ + return; + } + + this.$scope.displayHeaders = newHeaders; + if (this.$scope.enableFilter) { + this.$scope.filters = {}; + } + // Reset column sort information unless the new headers + // contain the column current sorted on. + if (this.$scope.enableSort && newHeaders.indexOf(this.$scope.sortColumn) === -1) { + this.$scope.sortColumn = undefined; + this.$scope.sortDirection = undefined; + } + this.updateRows(this.$scope.rows); + }; + + /** + * Read styles from the DOM and use them to calculate offsets + * for individual rows. + */ + MCTTableController.prototype.setElementSizes = function () { + 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'), + //row height is hard-coded for now. + rowHeight = 20, + overallHeight = headerHeight + (rowHeight * (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0)); + + this.$scope.columnWidths = []; + + while (column.length) { + this.$scope.columnWidths.push(column.prop('offsetWidth')); + column = column.next(); + } + this.$scope.headerHeight = headerHeight; + this.$scope.rowHeight = rowHeight; + this.$scope.totalHeight = overallHeight; + + this.$scope.visibleRows = this.$scope.displayRows.slice(0, this.maxDisplayRows).map(function(row, i) { + return { + rowIndex: i, + offsetY: (i * self.$scope.rowHeight) + self.$scope.headerHeight, + contents: row + }; + }); + + this.$scope.overrideRowPositioning = true; + }; + + /** + * Returns a new array which is a result of applying the sort + * criteria defined in $scope. + * + * Does not modify the array that was passed in. + */ + MCTTableController.prototype.sortRows = function(rowsToSort) { + /** + * Compare two variables, returning a number that represents + * which is larger. Similar to the default array sort + * comparator, but does not coerce all values to string before + * conversion. Strings are lowercased before comparison. + */ + function genericComparator(a, b) { + if (typeof a === "string" && typeof b === "string") { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + } + + if (!this.$scope.sortColumn || !this.$scope.sortDirection) { + return rowsToSort; + } + var sortKey = this.$scope.sortColumn, + sortDirectionMultiplier; + + if (this.$scope.sortDirection === 'asc') { + sortDirectionMultiplier = 1; + } else if (this.$scope.sortDirection === 'desc') { + sortDirectionMultiplier = -1; + } + + return rowsToSort.slice(0).sort(function(a, b) { + //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 genericComparator(valA, valB) * + sortDirectionMultiplier; + }); + }; + + /** + * Returns an object which contains the largest values + * for each key in the given set of rows. This is used to + * pre-calculate optimal column sizes without having to render + * every row. + */ + MCTTableController.prototype.findLargestRow = function(rows) { + var largestRow = rows.reduce(function (largestRow, row) { + Object.keys(row).forEach(function (key) { + var currentColumn = row[key].text, + currentColumnLength = + (currentColumn && currentColumn.length) ? + currentColumn.length : + currentColumn, + largestColumn = largestRow[key].text, + largestColumnLength = + (largestColumn && largestColumn.length) ? + largestColumn.length : + largestColumn; + + if (currentColumnLength > largestColumnLength) { + largestRow[key] = JSON.parse(JSON.stringify(row[key])); + } + }); + return largestRow; + }, JSON.parse(JSON.stringify(rows[0] || {}))); + + // Pad with characters to accomodate variable-width fonts, + // and remove characters that would allow word-wrapping. + largestRow = JSON.parse(JSON.stringify(largestRow)); + 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; + }; + + MCTTableController.prototype.resize = function (){ + var largestRow = this.findLargestRow(this.$scope.displayRows); + this.$scope.visibleRows = [ + { + rowIndex: 0, + offsetY: undefined, + contents: largestRow + } + ]; + + this.$timeout(this.setElementSizes.bind(this), 0); + }; + + /** + * Update rows with new data. If filtering is enabled, rows + * will be sorted before display. + */ + MCTTableController.prototype.updateRows = function (newRows) { + var displayRows = newRows; + this.$scope.visibleRows = []; + this.$scope.overrideRowPositioning = false; + + if (!this.$scope.displayHeaders) { + return; + } + + if (this.$scope.enableFilter) { + displayRows = this.filterRows(displayRows); + } + + if (this.$scope.enableSort) { + displayRows = this.sortRows(displayRows); + } + this.$scope.displayRows = displayRows; + this.resize(); + }; + + /** + * Filter rows. + */ + MCTTableController.prototype.filterRows = function(rowsToFilter) { + var filters = {}, + self = this; + + /** + * Returns true if row matches all filters. + */ + function matchRow(filters, row) { + return Object.keys(filters).every(function(key) { + if (!row[key]) { + return false; + } + var testVal = String(row[key].text).toLowerCase(); + return testVal.indexOf(filters[key]) !== -1; + }); + } + + if (!Object.keys(this.$scope.filters).length) { + return rowsToFilter; + } + + Object.keys(this.$scope.filters).forEach(function(key) { + if (!self.$scope.filters[key]) { + return; + } + filters[key] = self.$scope.filters[key].toLowerCase(); + }); + + return rowsToFilter.filter(matchRow.bind(null, filters)); + }; + + + return MCTTableController; + } +); diff --git a/platform/features/table/src/controllers/TableController.js b/platform/features/table/src/controllers/TableController.js new file mode 100644 index 0000000000..3b723836ae --- /dev/null +++ b/platform/features/table/src/controllers/TableController.js @@ -0,0 +1,214 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +/** + * This bundle adds a table view for displaying telemetry data. + * @namespace platform/features/table + */ +define( + [ + '../Table', + '../NameColumn' + ], + function (Table, NameColumn) { + "use strict"; + + /** + * The TableController is responsible for getting data onto the page + * in the table widget. This includes handling composition, + * configuration, and telemetry subscriptions. + * @memberof platform/features/table + * @param $scope + * @param telemetryHandler + * @param telemetryFormatter + * @constructor + */ + function TableController( + $scope, + telemetryHandler, + telemetryFormatter + ) { + var self = this; + + this.$scope = $scope; + this.columns = {}; //Range and Domain columns + this.handle = undefined; + //this.pending = false; + this.telemetryHandler = telemetryHandler; + this.table = new Table($scope.domainObject, telemetryFormatter); + this.changeListeners = []; + + $scope.rows = []; + + // Subscribe to telemetry when a domain object becomes available + this.$scope.$watch('domainObject', function(domainObject){ + if (!domainObject) + return; + + self.subscribe(); + self.registerChangeListeners(); + }); + + // Unsubscribe when the plot is destroyed + this.$scope.$on("$destroy", this.destroy.bind(this)); + } + + TableController.prototype.registerChangeListeners = function() { + //Defer registration of change listeners until domain object is + // available in order to avoid race conditions + + 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('domainObject.getModel().composition', this.subscribe.bind(this))); + + //Change of bounds in time conductor + this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', this.subscribe.bind(this))); + + }; + + /** + * Release the current subscription (called when scope is destroyed) + */ + TableController.prototype.destroy = function () { + if (this.handle) { + this.handle.unsubscribe(); + this.handle = undefined; + } + }; + + /** + Create a new subscription. This is called when + */ + TableController.prototype.subscribe = function() { + var self = this; + + /*if (this.pending){ + return; + }*/ + //this.pending = true; + + if (this.handle) { + this.handle.unsubscribe(); + } + + this.$scope.rows = []; + + //Noop because not supporting realtime data right now + function noop(){ + //self.pending = false; + } + + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( + this.$scope.domainObject, + noop, + true // Lossless + ); + + this.handle.request({}, this.addHistoricalData.bind(this)); + + this.setup(); + }; + + /** + * Add any historical data available + */ + TableController.prototype.addHistoricalData = function(domainObject, series) { + var i; + //this.pending = false; + for (i=0; i < series.getPointCount(); i++) { + this.updateRows(domainObject, this.handle.makeDatum(domainObject, series, i)); + } + }; + + /** + * Set the established configuration on the domain object + * @private + */ + TableController.prototype.writeConfigToModel = function (configuration) { + this.$scope.domainObject.useCapability('mutation', function (model) { + model.configuration = model.configuration || {}; + model.configuration.table = model.configuration.table || {}; + model.configuration.table.columns = configuration; + }); + }; + + /** + * Setup table columns based on domain object metadata + */ + TableController.prototype.setup = function() { + var handle = this.handle, + table = this.table, + self = this, + configuration; + + if (handle) { + handle.promiseTelemetryObjects().then(function () { + table.buildColumns(handle.getMetadata()); + + if (table.columns.length > 0){ + table.addColumn(new NameColumn(), 0); + } + + 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) + )); + }); + } + }; + + /** + * Add data to rows + * @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 + */ + TableController.prototype.updateRows = function (object, datum) { + this.$scope.rows.push(this.table.getRowValues(object, datum)); + }; + + /** + * When column configuration changes, update the visible headers + * accordingly. + */ + TableController.prototype.filterColumns = function () { + var config = this.table.getColumnConfiguration(); + + this.writeConfigToModel(config); + //Populate headers with visible columns (determined by configuration) + this.$scope.headers = Object.keys(config).filter(function(column) { + return config[column]; + }); + }; + + return TableController; + } +); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js new file mode 100644 index 0000000000..eb6ef2ef9c --- /dev/null +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -0,0 +1,94 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Notes on implementation of plot options + * + * Multiple y-axes will have to be handled with multiple forms as + * they will need to be stored on distinct model object + * + * Likewise plot series options per-child will need to be separate + * forms. + */ + + /** + * The LayoutController is responsible for supporting the + * Layout view. It arranges frames according to saved configuration + * and provides methods for updating these based on mouse + * movement. + * @memberof platform/features/plot + * @constructor + * @param {Scope} $scope the controller's Angular scope + */ + function TableOptionsController($scope) { + + var self = this; + + this.$scope = $scope; + this.domainObject = $scope.domainObject; + + $scope.columnsForm = {}; + + this.domainObject.getCapability('mutation').listen(function (model) { + self.populateForm(model); + }); + + $scope.$watchCollection('configuration.table.columns', function(columns){ + if (columns){ + self.domainObject.useCapability('mutation', function(model) { + model.configuration.table.columns = columns; + }); + self.domainObject.getCapability('persistence').persist(); + } + }); + + } + + TableOptionsController.prototype.populateForm = function (model) { + var columnsDefinition = (((model.configuration || {}).table || {}).columns || {}), + rows = []; + this.$scope.columnsForm = { + 'name':'Columns', + 'sections': [{ + 'name': 'Columns', + 'rows': rows + }]}; + + Object.keys(columnsDefinition).forEach(function (key){ + rows.push({ + 'name': key, + 'control': 'checkbox', + 'key': key + }); + }); + this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration)); + }; + + return TableOptionsController; + } +); diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js new file mode 100644 index 0000000000..1e3eec6111 --- /dev/null +++ b/platform/features/table/src/directives/MCTTable.js @@ -0,0 +1,24 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function MCTTable($timeout) { + return { + restrict: "E", + templateUrl: "platform/features/table/res/templates/mct-data-table.html", + controller: 'MCTTableController', + scope: { + headers: "=", + rows: "=", + enableFilter: "=?", + enableSort: "=?" + } + }; + } + + return MCTTable; + } +); diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js new file mode 100644 index 0000000000..bb15f9d55e --- /dev/null +++ b/platform/features/table/test/DomainColumnSpec.js @@ -0,0 +1,84 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/DomainColumn"], + function (DomainColumn) { + "use strict"; + + var TEST_DOMAIN_VALUE = "some formatted domain value"; + + describe("A domain column", function () { + var mockDataSet, + testMetadata, + mockFormatter, + column; + + beforeEach(function () { + mockDataSet = jasmine.createSpyObj( + "data", + [ "getDomainValue" ] + ); + mockFormatter = jasmine.createSpyObj( + "formatter", + [ "formatDomainValue", "formatRangeValue" ] + ); + testMetadata = { + key: "testKey", + name: "Test Name" + }; + 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"); + }); + + xit("looks up data from a data set", function () { + column.getValue(undefined, mockDataSet, 42); + expect(mockDataSet.getDomainValue) + .toHaveBeenCalledWith(42, "testKey"); + }); + + xit("formats domain values as time", function () { + mockDataSet.getDomainValue.andReturn(402513731000); + + // Should have just given the value the formatter gave + expect(column.getValue(undefined, mockDataSet, 42).text) + .toEqual(TEST_DOMAIN_VALUE); + + // Make sure that service interactions were as expected + expect(mockFormatter.formatDomainValue) + .toHaveBeenCalledWith(402513731000); + expect(mockFormatter.formatRangeValue) + .not.toHaveBeenCalled(); + }); + + }); + } +); diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js new file mode 100644 index 0000000000..355ebef545 --- /dev/null +++ b/platform/features/table/test/NameColumnSpec.js @@ -0,0 +1,58 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/NameColumn"], + function (NameColumn) { + "use strict"; + + 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 new file mode 100644 index 0000000000..b77245bb82 --- /dev/null +++ b/platform/features/table/test/RangeColumnSpec.js @@ -0,0 +1,76 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/RangeColumn"], + function (RangeColumn) { + "use strict"; + + 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/TableSpec.js b/platform/features/table/test/TableSpec.js new file mode 100644 index 0000000000..f042ec6c1d --- /dev/null +++ b/platform/features/table/test/TableSpec.js @@ -0,0 +1,197 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +define( + [ + "../src/Table", + "../src/DomainColumn" + ], + function (Table, DomainColumn) { + "use strict"; + + describe("A table", function () { + var mockDomainObject, + mockTelemetryFormatter, + table, + mockModel; + + beforeEach(function () { + mockDomainObject = jasmine.createSpyObj('domainObject', + ['getModel', 'useCapability', 'getCapability'] + ); + mockModel = {}; + mockDomainObject.getModel.andReturn(mockModel); + mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', + [ + 'formatDomainValue', + 'formatRangeValue' + ]); + mockTelemetryFormatter.formatDomainValue.andCallFake(function(valueIn){ + return valueIn; + }); + mockTelemetryFormatter.formatRangeValue.andCallFake(function(valueIn){ + return valueIn; + }); + + table = new Table(mockDomainObject, 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); + }); + + describe("Building columns from telemetry metadata", function() { + var metadata = [{ + ranges: [ + { + name: 'Range 1', + key: 'range1' + }, + { + name: 'Range 2', + key: 'range2' + } + ], + domains: [ + { + name: 'Domain 1', + key: 'domain1', + format: 'utc' + }, + { + name: 'Domain 2', + key: 'domain2', + format: 'utc' + } + ] + }]; + + beforeEach(function() { + table.buildColumns(metadata); + }); + + it("populates the columns attribute", function() { + expect(table.columns.length).toBe(4); + }); + + it("Build columns populates columns with domains to the left", function() { + expect(table.columns[0] instanceof DomainColumn).toBeTruthy(); + expect(table.columns[1] instanceof DomainColumn).toBeTruthy(); + expect(table.columns[2] instanceof DomainColumn).toBeFalsy(); + }); + + it("Produces headers for each column based on title", function() { + var headers, + firstColumn = table.columns[0]; + + spyOn(firstColumn, 'getTitle'); + headers = table.getHeaders(); + expect(headers.length).toBe(4); + expect(firstColumn.getTitle).toHaveBeenCalled(); + }); + + it("Provides a default configuration with all columns" + + " visible", function() { + var configuration = table.getColumnConfiguration(); + + expect(configuration).toBeDefined(); + expect(Object.keys(configuration).every(function(key){ + return configuration[key]; + })); + }); + + it("Column configuration exposes persisted configuration", function() { + var tableConfig, + modelConfig = { + table: { + columns : { + 'Range 1': false + } + } + }; + mockModel.configuration = modelConfig; + + tableConfig = table.getColumnConfiguration(); + + expect(tableConfig).toBeDefined(); + expect(tableConfig['Range 1']).toBe(false); + }); + + describe('retrieving row values', function () { + var datum, + rowValues; + + beforeEach(function() { + datum = { + 'range1': 'range 1 value', + 'range2': 'range 2 value', + 'domain1': 0, + 'domain2': 1 + }; + rowValues = table.getRowValues(mockDomainObject, datum); + }); + + it("Returns a value for every column", function() { + expect(rowValues['Range 1'].text).toBeDefined(); + expect(rowValues['Range 1'].text).toEqual('range 1' + + ' value'); + }); + + it("Uses the telemetry formatter to appropriately format" + + " telemetry values", function() { + expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); + }); + }); + }); + }); + } +); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js new file mode 100644 index 0000000000..e4f8d170d7 --- /dev/null +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -0,0 +1,155 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +define( + [ + "../../src/controllers/MCTTableController" + ], + function (MCTTableController) { + "use strict"; + + describe('The MCTTable Controller', function() { + + var controller, + mockScope, + watches, + mockTimeout, + mockElement; + + function promise(value) { + return { + then: function (callback){ + return promise(callback(value)); + } + }; + } + + beforeEach(function() { + watches = {}; + + mockScope = jasmine.createSpyObj('scope', [ + '$watchCollection' + ]); + mockScope.$watchCollection.andCallFake(function(event, callback) { + watches[event] = callback; + }); + + mockElement = jasmine.createSpyObj('element', [ + 'find', + 'on' + ]); + mockElement.find.andReturn(mockElement); + + mockScope.displayHeaders = true; + mockTimeout = jasmine.createSpy('$timeout'); + + controller = new MCTTableController(mockScope, mockTimeout, mockElement); + }); + + it('Reacts to changes to filters, headers, and rows', function() { + expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function)); + expect(mockScope.$watchCollection).toHaveBeenCalledWith('headers', jasmine.any(Function)); + expect(mockScope.$watchCollection).toHaveBeenCalledWith('rows', jasmine.any(Function)); + }); + + describe('rows', function() { + var testRows = []; + beforeEach(function() { + testRows = [ + { + 'col1': {'text': 'row1 col1 match'}, + 'col2': {'text': 'def'}, + 'col3': {'text': 'row1 col3'} + }, + { + 'col1': {'text': 'row2 col1 match'}, + 'col2': {'text': 'abc'}, + 'col3': {'text': 'row2 col3'} + }, + { + 'col1': {'text': 'row3 col1'}, + 'col2': {'text': 'ghi'}, + 'col3': {'text': 'row3 col3'} + } + ]; + }); + + it('Filters results based on filter input', function() { + var filters = {}, + filteredRows; + + mockScope.filters = filters; + + filteredRows = controller.filterRows(testRows); + expect(filteredRows.length).toBe(3); + filters.col1 = 'row1'; + filteredRows = controller.filterRows(testRows); + expect(filteredRows.length).toBe(1); + filters.col1 = 'match'; + filteredRows = controller.filterRows(testRows); + expect(filteredRows.length).toBe(2); + }); + + it('Sets rows on scope when rows change', function() { + controller.updateRows(testRows); + expect(mockScope.displayRows.length).toBe(3); + expect(mockScope.displayRows).toEqual(testRows); + }); + + describe('sorting', function() { + var sortedRows; + + it('Sorts rows ascending', function() { + mockScope.sortColumn = 'col1'; + mockScope.sortDirection = 'asc'; + + sortedRows = controller.sortRows(testRows); + expect(sortedRows[0].col1.text).toEqual('row1 col1 match'); + expect(sortedRows[1].col1.text).toEqual('row2 col1' + + ' match'); + expect(sortedRows[2].col1.text).toEqual('row3 col1'); + + }); + + it('Sorts rows descending', function() { + mockScope.sortColumn = 'col1'; + mockScope.sortDirection = 'desc'; + + sortedRows = controller.sortRows(testRows); + expect(sortedRows[0].col1.text).toEqual('row3 col1'); + expect(sortedRows[1].col1.text).toEqual('row2 col1 match'); + expect(sortedRows[2].col1.text).toEqual('row1 col1 match'); + }); + it('Sorts rows descending based on selected sort column', function() { + mockScope.sortColumn = 'col2'; + mockScope.sortDirection = 'desc'; + + 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'); + }); + }); + }); + }); + }); diff --git a/platform/features/table/test/controllers/TableControllerSpec.js b/platform/features/table/test/controllers/TableControllerSpec.js new file mode 100644 index 0000000000..f87789eccf --- /dev/null +++ b/platform/features/table/test/controllers/TableControllerSpec.js @@ -0,0 +1,224 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +define( + [ + "../../src/controllers/TableController" + ], + function (TableController) { + "use strict"; + + describe('The Table Controller', function() { + var mockScope, + mockTelemetryHandler, + mockTelemetryHandle, + mockTelemetryFormatter, + mockDomainObject, + mockTable, + mockConfiguration, + watches, + controller; + + function promise(value) { + return { + then: function (callback){ + return promise(callback(value)); + } + }; + } + + 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; + }); + + mockConfiguration = { + 'range1': true, + 'range2': true, + 'domain1': true + }; + + mockTable = jasmine.createSpyObj('table', + [ + 'buildColumns', + 'getColumnConfiguration', + 'getRowValues' + ] + ); + mockTable.columns = []; + mockTable.getColumnConfiguration.andReturn(mockConfiguration); + + mockDomainObject= jasmine.createSpyObj('domainObject', [ + 'getCapability', + 'useCapability', + 'getModel' + ]); + mockDomainObject.getModel.andReturn({}); + + mockScope.domainObject = mockDomainObject; + + mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ + 'request', + 'promiseTelemetryObjects', + 'getMetadata', + 'unsubscribe', + 'makeDatum' + ]); + mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); + + mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ + 'handle' + ]); + mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); + + controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter); + 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('the controller makes use of the table', function() { + + it('to create column definitions from telemetry' + + ' metadata', function() { + controller.setup(); + expect(mockTable.buildColumns).toHaveBeenCalled(); + }); + + it('to create column configuration, which is written to the' + + ' object model', function() { + var mockModel = {}; + + controller.setup(); + expect(mockTable.getColumnConfiguration).toHaveBeenCalled(); + expect(mockDomainObject.useCapability).toHaveBeenCalledWith('mutation', jasmine.any(Function)); + + mockDomainObject.useCapability.mostRecentCall.args[1](mockModel); + expect(mockModel.configuration).toBeDefined(); + }); + }); + + 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); + controller.addHistoricalData(mockDomainObject, mockSeries); + + 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(); + }); + + }); + }); + } +); diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js new file mode 100644 index 0000000000..9de96b5f52 --- /dev/null +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -0,0 +1,105 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/ + +define( + [ + "../../src/controllers/TableOptionsController" + ], + function (TableOptionsController) { + "use strict"; + + describe('The Table Options Controller', function() { + var mockDomainObject, + mockCapability, + controller, + mockScope; + + function promise(value) { + return { + then: function (callback){ + return promise(callback(value)); + } + }; + } + + beforeEach(function() { + mockCapability = jasmine.createSpyObj('mutationCapability', [ + 'listen' + ]); + mockDomainObject = jasmine.createSpyObj('domainObject', [ + 'getCapability' + ]); + mockDomainObject.getCapability.andReturn(mockCapability); + mockScope = jasmine.createSpyObj('scope', [ + '$watchCollection' + ]); + mockScope.domainObject = mockDomainObject; + + controller = new TableOptionsController(mockScope); + }); + + it('Registers a listener for mutation events on the object', function() { + expect(mockCapability.listen).toHaveBeenCalled(); + }); + + it('Listens for changes to object composition and updates' + + ' options accordingly', function() { + expect(mockScope.$watchCollection).toHaveBeenCalledWith('configuration.table.columns', jasmine.any(Function)); + }); + + describe('Populates scope with a form definition based on provided' + + ' column configuration', function() { + var mockModel; + + beforeEach(function() { + mockModel = { + configuration: { + table: { + columns: { + 'column1': true, + 'column2': true, + 'column3': false, + 'column4': true, + } + } + } + }; + controller.populateForm(mockModel); + }); + + it('creates form on scope', function() { + expect(mockScope.columnsForm).toBeDefined(); + expect(mockScope.columnsForm.sections[0]).toBeDefined(); + expect(mockScope.columnsForm.sections[0].rows).toBeDefined(); + expect(mockScope.columnsForm.sections[0].rows.length).toBe(4); + }); + + it('presents columns as checkboxes', function() { + expect(mockScope.columnsForm.sections[0].rows.every(function(row){ + return row.control === 'checkbox'; + })).toBe(true); + }); + }); + }); + + }); \ No newline at end of file