diff --git a/example/generator/SinewaveLimitProvider.js b/example/generator/SinewaveLimitProvider.js index 2cef29448f..22bb6ccd6d 100644 --- a/example/generator/SinewaveLimitProvider.js +++ b/example/generator/SinewaveLimitProvider.js @@ -27,8 +27,14 @@ define([ ) { - var RED = 0.9, - YELLOW = 0.5, + var RED = { + sin: 0.9, + cos: 0.9 + }, + YELLOW = { + sin: 0.5, + cos: 0.5 + }, LIMITS = { rh: { cssClass: "s-limit-upr s-limit-red", @@ -67,17 +73,18 @@ define([ SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) { return { evaluate: function (datum, valueMetadata) { - var range = valueMetadata ? valueMetadata.key : 'sin' - if (datum[range] > RED) { + var range = valueMetadata && valueMetadata.key; + + if (datum[range] > RED[range]) { return LIMITS.rh; } - if (datum[range] < -RED) { + if (datum[range] < -RED[range]) { return LIMITS.rl; } - if (datum[range] > YELLOW) { + if (datum[range] > YELLOW[range]) { return LIMITS.yh; } - if (datum[range] < -YELLOW) { + if (datum[range] < -YELLOW[range]) { return LIMITS.yl; } } diff --git a/openmct.js b/openmct.js index 1e98ace22d..92202bab1f 100644 --- a/openmct.js +++ b/openmct.js @@ -38,6 +38,7 @@ var openmct = new MCT(); openmct.legacyRegistry = defaultRegistry; openmct.install(openmct.plugins.Plot()); +openmct.install(openmct.plugins.TelemetryTable()); if (typeof BUILD_CONSTANTS !== 'undefined') { openmct.install(buildInfo(BUILD_CONSTANTS)); diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js deleted file mode 100644 index 7e4c37ec75..0000000000 --- a/platform/features/table/bundle.js +++ /dev/null @@ -1,128 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define([ - "./src/directives/MCTTable", - "./src/controllers/TelemetryTableController", - "./src/controllers/TableOptionsController", - '../../commonUI/regions/src/Region', - '../../commonUI/browse/src/InspectorRegion', - "./res/templates/table-options-edit.html", - "./res/templates/telemetry-table.html", - "legacyRegistry" -], function ( - MCTTable, - TelemetryTableController, - TableOptionsController, - Region, - InspectorRegion, - tableOptionsEditTemplate, - telemetryTableTemplate, - legacyRegistry -) { - /** - * 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": "Telemetry Table", - "cssClass": "icon-tabular-realtime", - "description": "A table of values over a given time period. The table will be automatically updated with new values as they become available", - "priority": 861, - "features": "creation", - "delegates": [ - "telemetry" - ], - "inspector": "table-options-edit", - "contains": [ - { - "has": "telemetry" - } - ], - "model": { - "composition": [] - }, - "views": [ - "table" - ] - } - ], - "controllers": [ - { - "key": "TelemetryTableController", - "implementation": TelemetryTableController, - "depends": ["$scope", "$timeout", "openmct"] - }, - { - "key": "TableOptionsController", - "implementation": TableOptionsController, - "depends": ["$scope"] - } - - ], - "views": [ - { - "name": "Telemetry Table", - "key": "table", - "cssClass": "icon-tabular-realtime", - "template": telemetryTableTemplate, - "needs": [ - "telemetry" - ], - "delegation": true, - "editable": false - } - ], - "directives": [ - { - "key": "mctTable", - "implementation": MCTTable, - "depends": ["$timeout"] - } - ], - "representations": [ - { - "key": "table-options-edit", - "template": tableOptionsEditTemplate - } - ] - } - }); - -}); diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html deleted file mode 100644 index 69ab92ef35..0000000000 --- a/platform/features/table/res/templates/mct-table.html +++ /dev/null @@ -1,95 +0,0 @@ -
- - Export - -
-
- - - - - - - - - -
- {{ header }} -
-
- - -
-
-
- - - - - - - -
{{header}}
- {{sizingRow[header].text}} -
-
-
- - - - - - - - - -
- -
- {{ 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 deleted file mode 100644 index df41e0ec3d..0000000000 --- a/platform/features/table/res/templates/table-options-edit.html +++ /dev/null @@ -1,32 +0,0 @@ - - -
- - -
diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html deleted file mode 100644 index d76de4c43c..0000000000 --- a/platform/features/table/res/templates/telemetry-table.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - -
diff --git a/platform/features/table/src/TableColumn.js b/platform/features/table/src/TableColumn.js deleted file mode 100644 index 971fd0e9ba..0000000000 --- a/platform/features/table/src/TableColumn.js +++ /dev/null @@ -1,67 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -define(function () { - function TableColumn(openmct, telemetryObject, metadatum) { - this.openmct = openmct; - this.telemetryObject = telemetryObject; - this.metadatum = metadatum; - this.formatter = openmct.telemetry.getValueFormatter(metadatum); - - this.titleValue = this.metadatum.name; - } - - TableColumn.prototype.title = function (title) { - if (arguments.length > 0) { - this.titleValue = title; - } - return this.titleValue; - }; - - TableColumn.prototype.isCurrentTimeSystem = function () { - var isCurrentTimeSystem = this.metadatum.hints.hasOwnProperty('domain') && - this.metadatum.key === this.openmct.time.timeSystem().key; - - return isCurrentTimeSystem; - }; - - TableColumn.prototype.hasValue = function (telemetryObject, telemetryDatum) { - var keyStringForDatum = this.openmct.objects.makeKeyString(telemetryObject.identifier); - var keyStringForColumn = this.openmct.objects.makeKeyString(this.telemetryObject.identifier); - return keyStringForDatum === keyStringForColumn && telemetryDatum.hasOwnProperty(this.metadatum.source); - }; - - TableColumn.prototype.getValue = function (telemetryDatum, limitEvaluator) { - var alarm = limitEvaluator && - limitEvaluator.evaluate(telemetryDatum, this.metadatum); - var value = { - text: this.formatter.format(telemetryDatum), - value: this.formatter.parse(telemetryDatum) - }; - - if (alarm) { - value.cssClass = alarm.cssClass; - } - return value; - }; - - return TableColumn; -}); diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js deleted file mode 100644 index d5525410aa..0000000000 --- a/platform/features/table/src/TableConfiguration.js +++ /dev/null @@ -1,164 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -/* global Set */ -define( - ['./TableColumn'], - function (TableColumn) { - - /** - * Class that manages table metadata, state, and contents. - * @memberof platform/features/table - * @param domainObject - * @constructor - */ - function TableConfiguration(domainObject, openmct) { - this.domainObject = domainObject; - this.openmct = openmct; - this.timeSystemColumn = undefined; - this.columns = []; - this.headers = new Set(); - this.timeSystemColumnTitle = undefined; - } - - /** - * Build column definition based on supplied telemetry metadata - * @param telemetryObject the telemetry producing object associated with this column - * @param metadata Metadata describing the domains and ranges available - * @returns {TableConfiguration} This object - */ - TableConfiguration.prototype.addColumn = function (telemetryObject, metadatum) { - var column = new TableColumn(this.openmct, telemetryObject, metadatum); - - if (column.isCurrentTimeSystem()) { - if (!this.timeSystemColumnTitle) { - this.timeSystemColumnTitle = column.title(); - } - column.title(this.timeSystemColumnTitle); - } - - this.columns.push(column); - this.headers.add(column.title()); - }; - - /** - * 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. - */ - TableConfiguration.prototype.getRowValues = function (telemetryObject, limitEvaluator, datum) { - return this.columns.reduce(function (rowObject, column) { - var columnTitle = column.title(); - var columnValue = { - text: '', - value: undefined - }; - if (rowObject[columnTitle] === undefined) { - rowObject[columnTitle] = columnValue; - } - - if (column.hasValue(telemetryObject, datum)) { - columnValue = column.getValue(datum, limitEvaluator); - - if (columnValue.text === undefined) { - columnValue.text = ''; - } - // Don't replace something with nothing. - // This occurs when there are multiple columns with the same - // column title - if (rowObject[columnTitle].text === undefined || - rowObject[columnTitle].text.length === 0) { - rowObject[columnTitle] = columnValue; - } - } - - return rowObject; - }, {}); - }; - - /** - * @private - */ - TableConfiguration.prototype.defaultColumnConfiguration = function () { - return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {}; - }; - - /** - * Set the established configuration on the domain object - * @private - */ - TableConfiguration.prototype.saveColumnConfiguration = function (columnConfig) { - this.domainObject.useCapability('mutation', function (model) { - model.configuration = model.configuration || {}; - model.configuration.table = model.configuration.table || {}; - model.configuration.table.columns = columnConfig; - }); - }; - - function configChanged(config1, config2) { - var config1Keys = Object.keys(config1), - config2Keys = Object.keys(config2); - - return (config1Keys.length !== config2Keys.length) || - config1Keys.some(function (key) { - return config1[key] !== config2[key]; - }); - } - - /** - * 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. - */ - TableConfiguration.prototype.buildColumnConfiguration = 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.headers.forEach(function (columnTitle) { - configuration[columnTitle] = - typeof defaultConfig[columnTitle] === 'undefined' ? true : - defaultConfig[columnTitle]; - }); - - //Synchronize column configuration with model - if (this.domainObject.hasCapability('editor') && - this.domainObject.getCapability('editor').isEditContextRoot() && - configChanged(configuration, defaultConfig)) { - this.saveColumnConfiguration(configuration); - } - - return configuration; - }; - - return TableConfiguration; - } -); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js deleted file mode 100644 index 69eab0daed..0000000000 --- a/platform/features/table/src/TelemetryCollection.js +++ /dev/null @@ -1,249 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - 'lodash', - 'EventEmitter' - ], - function (_, EventEmitter) { - - /** - * @constructor - */ - function TelemetryCollection() { - EventEmitter.call(this, arguments); - this.dupeCheck = false; - this.telemetry = []; - this.highBuffer = []; - this.sortField = undefined; - this.lastBounds = {}; - - _.bindAll(this, [ - 'addOne', - 'iteratee' - ]); - } - - TelemetryCollection.prototype = Object.create(EventEmitter.prototype); - - TelemetryCollection.prototype.iteratee = function (item) { - return _.get(item, this.sortField); - }; - - /** - * This function is optimized for ticking - it assumes that start and end - * bounds will only increase and as such this cannot be used for decreasing - * bounds changes. - * - * An implication of this is that data will not be discarded that exceeds - * the given end bounds. For arbitrary bounds changes, it's assumed that - * a telemetry requery is performed anyway, and the collection is cleared - * and repopulated. - * - * @fires TelemetryCollection#added - * @fires TelemetryCollection#discarded - * @param bounds - */ - TelemetryCollection.prototype.bounds = function (bounds) { - var startChanged = this.lastBounds.start !== bounds.start; - var endChanged = this.lastBounds.end !== bounds.end; - var startIndex = 0; - var endIndex = 0; - var discarded; - var added; - var testValue; - - this.lastBounds = bounds; - - // If collection is not sorted by a time field, we cannot respond to - // bounds events - if (this.sortField === undefined) { - this.lastBounds = bounds; - return; - } - - if (startChanged) { - testValue = _.set({}, this.sortField, bounds.start); - // Calculate the new index of the first item within the bounds - startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField); - discarded = this.telemetry.splice(0, startIndex); - } - if (endChanged) { - testValue = _.set({}, this.sortField, bounds.end); - // Calculate the new index of the last item in bounds - endIndex = _.sortedLastIndex(this.highBuffer, testValue, this.sortField); - added = this.highBuffer.splice(0, endIndex); - added.forEach(function (datum) { - this.telemetry.push(datum); - }.bind(this)); - } - - if (discarded && discarded.length > 0) { - /** - * A `discarded` event is emitted when telemetry data fall out of - * bounds due to a bounds change event - * @type {object[]} discarded the telemetry data - * discarded as a result of the bounds change - */ - this.emit('discarded', discarded); - } - if (added && added.length > 0) { - /** - * An `added` event is emitted when a bounds change results in - * received telemetry falling within the new bounds. - * @type {object[]} added the telemetry data that is now within bounds - */ - this.emit('added', added); - } - }; - - /** - * Adds an individual item to the collection. Used internally only - * @private - * @param item - */ - TelemetryCollection.prototype.addOne = function (item) { - var isDuplicate = false; - var boundsDefined = this.lastBounds && - (this.lastBounds.start !== undefined && this.lastBounds.end !== undefined); - var array; - var boundsLow; - var boundsHigh; - - // If collection is not sorted by a time field, we cannot respond to - // bounds events, so no bounds checking necessary - if (this.sortField === undefined) { - this.telemetry.push(item); - - return true; - } - - // Insert into either in-bounds array, or the out of bounds high buffer. - // Data in the high buffer will be re-evaluated for possible insertion on next tick - - if (boundsDefined) { - boundsHigh = _.get(item, this.sortField) > this.lastBounds.end; - boundsLow = _.get(item, this.sortField) < this.lastBounds.start; - - if (!boundsHigh && !boundsLow) { - array = this.telemetry; - } else if (boundsHigh) { - array = this.highBuffer; - } - } else { - array = this.telemetry; - } - - // If out of bounds low, disregard data - if (!boundsLow) { - // Going to check for duplicates. Bound the search problem to - // items around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // based on time stamp because the array is guaranteed ordered due - // to sorted insertion. - var startIx = _.sortedIndex(array, item, this.sortField); - var endIx; - - if (this.dupeCheck && startIx !== array.length) { - endIx = _.sortedLastIndex(array, item, this.sortField); - - // Create an array of potential dupes, based on having the - // same time stamp - var potentialDupes = array.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1; - } - - if (!isDuplicate) { - array.splice(endIx || startIx, 0, item); - - //Return true if it was added and in bounds - return array === this.telemetry; - } - } - return false; - }; - - /** - * Add an array of objects to this telemetry collection - * @fires TelemetryCollection#added - * @param {object[]} items - */ - TelemetryCollection.prototype.add = function (items) { - var added = items.filter(this.addOne); - this.emit('added', added); - this.dupeCheck = true; - }; - - /** - * Clears the contents of the telemetry collection - */ - TelemetryCollection.prototype.clear = function () { - this.telemetry = []; - this.highBuffer = []; - }; - - /** - * Sorts the telemetry collection based on the provided sort field - * specifier. Subsequent inserts are sorted to maintain specified sport - * order. - * - * @example - * // First build some mock telemetry for the purpose of an example - * let now = Date.now(); - * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { - * return { - * // define an object property to demonstrate nested paths - * timestamp: { - * ms: now - value * 1000, - * text: - * }, - * value: value - * } - * }); - * let collection = new TelemetryCollection(); - * - * collection.add(telemetry); - * - * // Sort by telemetry value - * collection.sort("value"); - * - * // Sort by ms since epoch - * collection.sort("timestamp.ms"); - * - * // Sort by formatted date text - * collection.sort("timestamp.text"); - * - * - * @param {string} sortField An object property path. - */ - TelemetryCollection.prototype.sort = function (sortField) { - this.sortField = sortField; - if (sortField !== undefined) { - this.telemetry = _.sortBy(this.telemetry, this.iteratee); - } - }; - - return TelemetryCollection; - } -); diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js deleted file mode 100644 index 7c8242b03e..0000000000 --- a/platform/features/table/src/controllers/MCTTableController.js +++ /dev/null @@ -1,828 +0,0 @@ - -define( - [ - 'zepto', - 'lodash' - ], - function ($, _) { - - /** - * A controller for the MCTTable directive. Populates scope with - * data used for populating, sorting, and filtering - * tables. - * @param $scope - * @param $timeout - * @param element - * @constructor - */ - function MCTTableController($scope, $window, element, exportService, formatService, openmct) { - var self = this; - - this.$scope = $scope; - this.element = $(element[0]); - this.$window = $window; - this.maxDisplayRows = 100; - - this.scrollable = this.element.find('.t-scrolling').first(); - this.resultsHeader = this.element.find('.mct-table>thead').first(); - this.sizingTableBody = this.element.find('.t-sizing-table>tbody').first(); - this.$scope.sizingRow = {}; - this.$scope.calcTableWidthPx = '100%'; - this.timeApi = openmct.time; - this.toiFormatter = undefined; - this.formatService = formatService; - this.callbacks = {}; - - //Bind all class functions to 'this' - _.bindAll(this, [ - 'addRows', - 'binarySearch', - 'buildLargestRow', - 'changeBounds', - 'changeTimeOfInterest', - 'changeTimeSystem', - 'destroyConductorListeners', - 'digest', - 'filterAndSort', - 'filterRows', - 'firstVisible', - 'insertSorted', - 'lastVisible', - 'onRowClick', - 'onScroll', - 'removeRows', - 'resize', - 'scrollToBottom', - 'scrollToRow', - 'setElementSizes', - 'setHeaders', - 'setRows', - 'setTimeOfInterestRow', - 'setVisibleRows', - 'sortComparator', - 'sortRows' - ]); - - this.scrollable.on('scroll', this.onScroll); - - $scope.visibleRows = []; - $scope.displayRows = []; - - /** - * 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; - } - if (scope.sortColumn !== undefined) { - scope.sortDirection = "asc"; - } - } - - setDefaults($scope); - - $scope.exportAsCSV = function () { - var headers = $scope.displayHeaders, - filename = $(element[0]).attr('export-as'); - - exportService.exportCSV($scope.displayRows.map(function (row) { - return headers.reduce(function (r, header) { - r[header] = row[header].text; - return r; - }, {}); - }), { - headers: headers, - filename: filename - }); - }; - - $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; - } else if ($scope.sortColumn !== undefined && - $scope.sortDirection === undefined) { - $scope.sortDirection = 'asc'; - } - self.setRows($scope.rows); - self.setTimeOfInterestRow(self.timeApi.timeOfInterest()); - }; - - /* - * Define watches to listen for changes to headers and rows. - */ - $scope.$watchCollection('filters', function () { - self.setRows($scope.rows); - }); - $scope.$watch('headers', function (newHeaders, oldHeaders) { - if (newHeaders !== oldHeaders) { - this.setHeaders(newHeaders); - } - }.bind(this)); - $scope.$watch('rows', this.setRows); - - /* - * Listen for rows added individually (eg. for real-time tables) - */ - $scope.$on('add:rows', this.addRows); - $scope.$on('remove:rows', this.removeRows); - - /** - * Populated from the default-sort attribute on MctTable - * directive tag. - */ - $scope.$watch('defaultSort', function (newColumn, oldColumn) { - if (newColumn !== oldColumn) { - $scope.toggleSort(newColumn); - } - }); - - /* - * Listen for resize events to trigger recalculation of table width - */ - $scope.resize = this.setElementSizes; - - /** - * Scope variable that is populated from the 'time-columns' - * attribute on the MctTable tag. Indicates which columns, while - * sorted, can be used for indicated time of interest. - */ - $scope.$watch("timeColumns", function (timeColumns) { - if (timeColumns) { - this.destroyConductorListeners(); - - this.timeApi.on('timeSystem', this.changeTimeSystem); - this.timeApi.on('timeOfInterest', this.changeTimeOfInterest); - this.timeApi.on('bounds', this.changeBounds); - - // If time system defined, set initially - if (this.timeApi.timeSystem() !== undefined) { - this.changeTimeSystem(this.timeApi.timeSystem()); - } - } - }.bind(this)); - - $scope.$on('$destroy', function () { - this.scrollable.off('scroll', this.onScroll); - this.destroyConductorListeners(); - - }.bind(this)); - } - - MCTTableController.prototype.destroyConductorListeners = function () { - this.timeApi.off('timeSystem', this.changeTimeSystem); - this.timeApi.off('timeOfInterest', this.changeTimeOfInterest); - this.timeApi.off('bounds', this.changeBounds); - }; - - MCTTableController.prototype.changeTimeSystem = function (timeSystem) { - var format = timeSystem.timeFormat; - this.toiFormatter = this.formatService.getFormat(format); - }; - - /** - * If auto-scroll is enabled, this function will scroll to the - * bottom of the page - * @private - */ - MCTTableController.prototype.scrollToBottom = function () { - this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight; - }; - - /** - * Handles a row add event. Rows can be added as needed using the - * `add:row` broadcast event. - * @private - */ - MCTTableController.prototype.addRows = function (event, rows) { - //Does the row pass the current filter? - if (this.filterRows(rows).length > 0) { - rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows)); - - //Resize the columns , then update the rows visible in the table - this.resize([this.$scope.sizingRow].concat(rows)) - .then(this.setVisibleRows) - .then(function () { - if (this.$scope.autoScroll) { - this.scrollToBottom(); - } - }.bind(this)); - - var toi = this.timeApi.timeOfInterest(); - if (toi !== -1) { - this.setTimeOfInterestRow(toi); - } - } - }; - - /** - * Handles a row remove event. Rows can be removed as needed using the - * `remove:row` broadcast event. - * @private - */ - MCTTableController.prototype.removeRows = function (event, rows) { - var indexInDisplayRows; - rows.forEach(function (row) { - // Do a sequential search here. Only way of finding row is by - // object equality, so array is in effect unsorted. - indexInDisplayRows = this.$scope.displayRows.indexOf(row); - if (indexInDisplayRows !== -1) { - this.$scope.displayRows.splice(indexInDisplayRows, 1); - } - }, this); - - this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows)); - - this.setElementSizes(); - this.setVisibleRows() - .then(function () { - if (this.$scope.autoScroll) { - this.scrollToBottom(); - } - }.bind(this)); - - }; - - /** - * @private - */ - MCTTableController.prototype.onScroll = function (event) { - this.scrollWindow = { - top: this.scrollable[0].scrollTop, - bottom: this.scrollable[0].scrollTop + this.scrollable[0].offsetHeight, - offsetHeight: this.scrollable[0].offsetHeight, - height: this.scrollable[0].scrollHeight - }; - this.$window.requestAnimationFrame(function () { - this.setVisibleRows(); - this.digest(); - - // If user scrolls away from bottom, disable auto-scroll. - // Auto-scroll will be re-enabled if user scrolls to bottom again. - if (this.scrollWindow.top < - (this.scrollWindow.height - this.scrollWindow.offsetHeight) - 20) { - this.$scope.autoScroll = false; - } else { - this.$scope.autoScroll = true; - } - this.scrolling = false; - delete this.scrollWindow; - }.bind(this)); - }; - - /** - * Return first visible row, based on current scroll state. - * @private - */ - MCTTableController.prototype.firstVisible = function () { - var topScroll = this.scrollWindow ? - this.scrollWindow.top : - this.scrollable[0].scrollTop; - - return Math.floor( - (topScroll) / this.$scope.rowHeight - ); - - }; - - /** - * Return last visible row, based on current scroll state. - * @private - */ - MCTTableController.prototype.lastVisible = function () { - var bottomScroll = this.scrollWindow ? - this.scrollWindow.bottom : - this.scrollable[0].scrollTop + this.scrollable[0].offsetHeight; - - return Math.ceil( - (bottomScroll) / - this.$scope.rowHeight - ); - }; - - /** - * Sets visible rows based on array - * content and current scroll state. - */ - MCTTableController.prototype.setVisibleRows = function () { - var self = this, - totalVisible, - numberOffscreen, - firstVisible, - lastVisible, - start, - end; - - //No need to scroll - if (this.$scope.displayRows.length < this.maxDisplayRows) { - start = 0; - end = this.$scope.displayRows.length; - } else { - firstVisible = this.firstVisible(); - lastVisible = this.lastVisible(); - 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 = Math.min(this.maxDisplayRows, - this.$scope.displayRows.length); - } else if (end >= this.$scope.displayRows.length) { - end = this.$scope.displayRows.length; - start = end - this.maxDisplayRows + 1; - } - if (this.$scope.visibleRows[0] && - this.$scope.visibleRows[0].rowIndex === start && - this.$scope.visibleRows[this.$scope.visibleRows.length - 1] - .rowIndex === end) { - return this.digest(); - } - } - //Set visible rows from display rows, based on calculated offset. - this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) - .map(function (row, i) { - return { - rowIndex: start + i, - offsetY: ((start + i) * self.$scope.rowHeight), - contents: row - }; - }); - return this.digest(); - }; - - /** - * Update table headers with new headers. If filtering is - * enabled, reset filters. If sorting is enabled, reset - * sorting. - */ - MCTTableController.prototype.setHeaders = 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 currently sorted on. - if (this.$scope.enableSort && - newHeaders.indexOf(this.$scope.sortColumn) === -1) { - this.$scope.sortColumn = undefined; - this.$scope.sortDirection = undefined; - } - this.setRows(this.$scope.rows); - }; - - /** - * Read styles from the DOM and use them to calculate offsets - * for individual rows. - */ - MCTTableController.prototype.setElementSizes = function () { - var tbody = this.sizingTableBody, - firstRow = tbody.find('tr'), - column = firstRow.find('td'), - rowHeight = firstRow.prop('offsetHeight'), - columnWidth, - tableWidth = 0, - overallHeight = (rowHeight * - (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0)); - - this.$scope.columnWidths = []; - - while (column.length) { - columnWidth = column.prop('offsetWidth'); - this.$scope.columnWidths.push(column.prop('offsetWidth')); - tableWidth += columnWidth; - column = column.next(); - } - this.$scope.rowHeight = rowHeight; - this.$scope.totalHeight = overallHeight; - - var scrollW = this.scrollable[0].offsetWidth - this.scrollable[0].clientWidth; - if (scrollW && scrollW > 0) { - this.$scope.calcTableWidthPx = 'calc(100% - ' + scrollW + 'px)'; - } - - if (tableWidth > 0) { - this.$scope.totalWidth = tableWidth + 'px'; - } else { - this.$scope.totalWidth = 'none'; - } - }; - - /** - * Finds the correct insertion point for a new row, which takes into - * account duplicates to make sure new rows are inserted in a way that - * maintains arrival order. - * - * @private - * @param {Array} searchArray - * @param {Object} searchElement Object to find the insertion point for - */ - MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) { - var index; - var testIndex; - var first = searchArray[0]; - var last = searchArray[searchArray.length - 1]; - - if (first) { - first = first[this.$scope.sortColumn].text; - } - if (last) { - last = last[this.$scope.sortColumn].text; - } - // Shortcut check for append/prepend - if (first && this.sortComparator(first, searchElement) >= 0) { - index = testIndex = 0; - } else if (last && this.sortComparator(last, searchElement) <= 0) { - index = testIndex = searchArray.length; - } else { - // use a binary search to find the correct insertion point - index = testIndex = this.binarySearch( - searchArray, - searchElement, - 0, - searchArray.length - 1 - ); - } - - //It's possible that the insertion point is a duplicate of the element to be inserted - var isDupe = function () { - return this.sortComparator(searchElement, - searchArray[testIndex][this.$scope.sortColumn].text) === 0; - }.bind(this); - - // In the event of a duplicate, scan left or right (depending on - // sort order) to find an insertion point that maintains order received - while (testIndex >= 0 && testIndex < searchArray.length && isDupe()) { - if (this.$scope.sortDirection === 'asc') { - index = ++testIndex; - } else { - index = testIndex--; - } - } - return index; - }; - - /** - * @private - */ - MCTTableController.prototype.binarySearch = function (searchArray, searchElement, min, max) { - var sampleAt = Math.floor((max - min) / 2) + min; - - if (max < min) { - return min; // Element is not in array, min gives direction - } - switch (this.sortComparator(searchElement, - searchArray[sampleAt][this.$scope.sortColumn].text)) { - case -1: - return this.binarySearch(searchArray, searchElement, min, - sampleAt - 1); - case 0: - return sampleAt; - case 1: - return this.binarySearch(searchArray, searchElement, - sampleAt + 1, max); - } - }; - - /** - * @private - */ - MCTTableController.prototype.insertSorted = function (array, element) { - var index = -1; - - if (!this.$scope.sortColumn || !this.$scope.sortDirection) { - //No sorting applied, push it on the end. - index = array.length; - } else { - //Sort is enabled, perform binary search to find insertion point - index = this.findInsertionPoint(array, element[this.$scope.sortColumn].text); - } - if (index === -1) { - array.unshift(element); - } else if (index === array.length) { - array.push(element); - } else { - array.splice(index, 0, element); - } - }; - - /** - * 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. - * - * @private - */ - 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; - } - - 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(); - } - - if (a < b) { - result = -1; - } - if (a > b) { - result = 1; - } - - if (this.$scope.sortDirection === 'asc') { - sortDirectionMultiplier = 1; - } else if (this.$scope.sortDirection === 'desc') { - sortDirectionMultiplier = -1; - } - - return result * sortDirectionMultiplier; - }; - - /** - * 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) { - var self = this, - sortKey = this.$scope.sortColumn; - - if (!this.$scope.sortColumn || !this.$scope.sortDirection) { - return rowsToSort; - } - - return rowsToSort.sort(function (a, b) { - return self.sortComparator(a[sortKey].text, b[sortKey].text); - }); - }; - - /** - * 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.buildLargestRow = function (rows) { - var largestRow = rows.reduce(function (prevLargest, row) { - Object.keys(row).forEach(function (key) { - var currentColumn, - currentColumnLength, - largestColumn, - largestColumnLength; - if (row[key]) { - currentColumn = (row[key]).text; - currentColumnLength = - (currentColumn && currentColumn.length) ? - currentColumn.length : - currentColumn; - largestColumn = prevLargest[key] ? prevLargest[key].text : ""; - largestColumnLength = largestColumn.length; - - if (currentColumnLength > largestColumnLength) { - prevLargest[key] = JSON.parse(JSON.stringify(row[key])); - } - } - }); - return prevLargest; - }, JSON.parse(JSON.stringify(rows[0] || {}))); - return largestRow; - }; - - // Will effectively cap digests at 60Hz - // Also turns digest into a promise allowing code to force digest, then - // schedule something to happen afterwards - MCTTableController.prototype.digest = function () { - var scope = this.$scope; - var self = this; - var raf = this.$window.requestAnimationFrame; - var promise = this.digestPromise; - - if (!promise) { - self.digestPromise = promise = new Promise(function (resolve) { - raf(function () { - scope.$digest(); - self.digestPromise = undefined; - resolve(); - }); - }); - } - - return promise; - }; - - /** - * 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. - * @private - */ - MCTTableController.prototype.resize = function (rows) { - this.$scope.sizingRow = this.buildLargestRow(rows); - return this.digest().then(this.setElementSizes); - }; - - /** - * @private - */ - MCTTableController.prototype.filterAndSort = function (rows) { - var displayRows = rows; - if (this.$scope.enableFilter) { - displayRows = this.filterRows(displayRows); - } - - if (this.$scope.enableSort) { - displayRows = this.sortRows(displayRows.slice(0)); - } - return displayRows; - }; - - /** - * Update rows with new data. If filtering is enabled, rows - * will be sorted before display. - */ - MCTTableController.prototype.setRows = function (newRows) { - //Nothing to show because no columns visible - if (!this.$scope.displayHeaders || !newRows) { - return; - } - - this.$scope.displayRows = this.filterAndSort(newRows || []); - return this.resize(newRows) - .then(function (rows) { - return this.setVisibleRows(rows); - }.bind(this)) - //Timeout following setVisibleRows to allow digest to - // perform DOM changes, otherwise scrollTo won't work. - .then(function () { - //If TOI specified, scroll to it - var timeOfInterest = this.timeApi.timeOfInterest(); - if (timeOfInterest) { - this.setTimeOfInterestRow(timeOfInterest); - this.scrollToRow(this.$scope.toiRowIndex); - } - }.bind(this)); - }; - - /** - * Applies user defined filters to rows. These filters are based on - * the text entered in the search areas in each column. - * @param rowsToFilter {Object[]} The rows to apply filters to - * @returns {Object[]} A filtered copy of the supplied rows - */ - MCTTableController.prototype.filterRows = function (rowsToFilter) { - var filters = {}, - self = this; - - /** - * Returns true if row matches all filters. - */ - function matchRow(filterMap, row) { - return Object.keys(filterMap).every(function (key) { - if (!row[key]) { - return false; - } - var testVal = String(row[key].text).toLowerCase(); - return testVal.indexOf(filterMap[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)); - }; - - /** - * Scroll the view to a given row index - * @param displayRowIndex {number} The index in the displayed rows - * to scroll to. - */ - MCTTableController.prototype.scrollToRow = function (displayRowIndex) { - - var visible = displayRowIndex > this.firstVisible() && displayRowIndex < this.lastVisible(); - - if (!visible) { - var scrollTop = displayRowIndex * this.$scope.rowHeight + - (this.scrollable[0].offsetHeight / 2); - this.scrollable[0].scrollTop = scrollTop; - this.setVisibleRows(); - } - }; - - /** - * Update rows with new data. If filtering is enabled, rows - * will be sorted before display. - */ - MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) { - var isSortedByTime = - this.$scope.timeColumns && - this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1; - - this.$scope.toiRowIndex = -1; - - if (newTOI && isSortedByTime) { - var formattedTOI = this.toiFormatter.format(newTOI); - var rowIndex = this.binarySearch( - this.$scope.displayRows, - formattedTOI, - 0, - this.$scope.displayRows.length - 1); - - if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) { - this.$scope.toiRowIndex = rowIndex; - } - } - }; - - MCTTableController.prototype.changeTimeOfInterest = function (newTOI) { - this.setTimeOfInterestRow(newTOI); - this.scrollToRow(this.$scope.toiRowIndex); - }; - - /** - * On zoom, pan, etc. reset TOI - * @param bounds - */ - MCTTableController.prototype.changeBounds = function (bounds) { - this.setTimeOfInterestRow(this.timeApi.timeOfInterest()); - if (this.$scope.toiRowIndex !== -1) { - this.scrollToRow(this.$scope.toiRowIndex); - } - }; - - /** - * @private - */ - MCTTableController.prototype.onRowClick = function (event, rowIndex) { - if (this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1) { - var selectedTime = this.$scope.displayRows[rowIndex][this.$scope.sortColumn].text; - if (selectedTime && - this.toiFormatter.validate(selectedTime) && - event.altKey) { - this.timeApi.timeOfInterest(this.toiFormatter.parse(selectedTime)); - } - } - }; - - return MCTTableController; - } -); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js deleted file mode 100644 index a9d37c924d..0000000000 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ /dev/null @@ -1,113 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [], - function () { - - /** - * 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; - 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); - })); - }); - - /** - * Maintain a configuration object on scope that stores column - * configuration. On change, synchronize with object model. - */ - $scope.$watchCollection('configuration.table.columns', function (newColumns, oldColumns) { - if (newColumns !== oldColumns) { - self.domainObject.useCapability('mutation', function (model) { - model.configuration.table.columns = newColumns; - }); - self.domainObject.getCapability('persistence').persist(); - } - }); - - /** - * Destroy all mutation listeners - */ - $scope.$on('$destroy', unlisten); - - } - - 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/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js deleted file mode 100644 index 1789b27554..0000000000 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ /dev/null @@ -1,450 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -/* global console*/ - -/** - * This bundle adds a table view for displaying telemetry data. - * @namespace platform/features/table - */ -define( - [ - '../TableConfiguration', - '../../../../../src/api/objects/object-utils', - '../TelemetryCollection', - 'lodash' - - ], - function (TableConfiguration, objectUtils, TelemetryCollection, _) { - - /** - * 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 - * @constructor - */ - function TelemetryTableController( - $scope, - $timeout, - openmct - ) { - - this.$scope = $scope; - this.$timeout = $timeout; - this.openmct = openmct; - this.batchSize = 1000; - - /* - * Initialization block - */ - this.columns = {}; //Range and Domain columns - this.unobserveObject = undefined; - this.subscriptions = []; - this.timeColumns = []; - $scope.rows = []; - this.table = new TableConfiguration($scope.domainObject, - openmct); - this.lastBounds = this.openmct.time.bounds(); - this.lastRequestTime = 0; - this.telemetry = new TelemetryCollection(); - if (this.lastBounds) { - this.telemetry.bounds(this.lastBounds); - } - - /* - * Create a new format object from legacy object, and replace it - * when it changes - */ - this.domainObject = objectUtils.toNewFormat($scope.domainObject.getModel(), - $scope.domainObject.getId()); - - this.$scope.exportAs = this.$scope.domainObject.getModel().name; - - _.bindAll(this, [ - 'destroy', - 'sortByTimeSystem', - 'loadColumns', - 'getHistoricalData', - 'subscribeToNewData', - 'changeBounds', - 'setClock', - 'addRowsToTable', - 'removeRowsFromTable' - ]); - - // Retrieve data when domain object is available. - // Also deferring telemetry request makes testing easier as controller - // construction has no unintended consequences. - $scope.$watch("domainObject", function () { - this.getData(); - this.registerChangeListeners(); - }.bind(this)); - - this.setClock(this.openmct.time.clock()); - - this.$scope.$on("$destroy", this.destroy); - } - - /** - * @private - * @param {boolean} scroll - */ - TelemetryTableController.prototype.setClock = function (clock) { - this.$scope.autoScroll = clock !== undefined; - }; - - /** - * Based on the selected time system, find a matching domain column - * to sort by. By default will just match on key. - * - * @private - */ - TelemetryTableController.prototype.sortByTimeSystem = function () { - var scope = this.$scope; - var sortColumn; - scope.defaultSort = undefined; - - sortColumn = this.table.columns.filter(function (column) { - return column.isCurrentTimeSystem(); - })[0]; - if (sortColumn) { - scope.defaultSort = sortColumn.title(); - this.telemetry.sort(sortColumn.title() + '.value'); - } - }; - - /** - * Attaches listeners that respond to state change in domain object, - * conductor, and receipt of telemetry - * - * @private - */ - TelemetryTableController.prototype.registerChangeListeners = function () { - if (this.unobserveObject) { - this.unobserveObject(); - } - - this.unobserveObject = this.openmct.objects.observe(this.domainObject, "*", - function (domainObject) { - this.domainObject = domainObject; - this.getData(); - }.bind(this) - ); - - this.openmct.time.on('timeSystem', this.sortByTimeSystem); - this.openmct.time.on('bounds', this.changeBounds); - this.openmct.time.on('clock', this.setClock); - - this.telemetry.on('added', this.addRowsToTable); - this.telemetry.on('discarded', this.removeRowsFromTable); - }; - - /** - * On receipt of new telemetry, informs mct-table directive that new rows - * are available and passes populated rows to it - * - * @private - * @param rows - */ - TelemetryTableController.prototype.addRowsToTable = function (rows) { - this.$scope.$broadcast('add:rows', rows); - }; - - /** - * When rows are to be removed, informs mct-table directive. Row removal - * happens when rows call outside the bounds of the time conductor - * - * @private - * @param rows - */ - TelemetryTableController.prototype.removeRowsFromTable = function (rows) { - this.$scope.$broadcast('remove:rows', rows); - }; - - /** - * On Time Conductor bounds change, update displayed telemetry. In the - * case of a tick, previously visible telemetry that is now out of band - * will be removed from the table. - * @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds - */ - TelemetryTableController.prototype.changeBounds = function (bounds, isTick) { - if (isTick) { - this.telemetry.bounds(bounds); - } else { - // Is fixed bounds change - this.getData(); - } - this.lastBounds = bounds; - }; - - /** - * Clean controller, deregistering listeners etc. - */ - TelemetryTableController.prototype.destroy = function () { - - this.openmct.time.off('timeSystem', this.sortByTimeSystem); - this.openmct.time.off('bounds', this.changeBounds); - this.openmct.time.off('clock', this.setClock); - - this.subscriptions.forEach(function (subscription) { - subscription(); - }); - - if (this.unobserveObject) { - this.unobserveObject(); - } - this.subscriptions = []; - - if (this.timeoutHandle) { - this.$timeout.cancel(this.timeoutHandle); - } - }; - - /** - * For given objects, populate column metadata and table headers. - * @private - * @param {module:openmct.DomainObject[]} objects the domain objects for - * which columns should be populated - */ - TelemetryTableController.prototype.loadColumns = function (objects) { - var telemetryApi = this.openmct.telemetry; - - this.table = new TableConfiguration(this.$scope.domainObject, - this.openmct); - - this.$scope.headers = []; - - if (objects.length > 0) { - objects.forEach(function (object) { - var metadataValues = telemetryApi.getMetadata(object).values(); - metadataValues.forEach(function (metadatum) { - this.table.addColumn(object, metadatum); - }.bind(this)); - }.bind(this)); - - this.filterColumns(); - this.sortByTimeSystem(); - } - - return objects; - }; - - /** - * Request telemetry data from an historical store for given objects. - * @private - * @param {object[]} The domain objects to request telemetry for - * @returns {Promise} resolved when historical data is available - */ - TelemetryTableController.prototype.getHistoricalData = function (objects) { - var self = this; - var openmct = this.openmct; - var bounds = openmct.time.bounds(); - var scope = this.$scope; - var rowData = []; - var processedObjects = 0; - var requestTime = this.lastRequestTime = Date.now(); - var telemetryCollection = this.telemetry; - - var promise = new Promise(function (resolve, reject) { - /* - * On completion of batched processing, set the rows on scope - */ - function finishProcessing() { - telemetryCollection.add(rowData); - scope.rows = telemetryCollection.telemetry; - self.loading(false); - - resolve(scope.rows); - } - - /* - * Process a batch of historical data - */ - function processData(object, historicalData, index, limitEvaluator) { - if (index >= historicalData.length) { - processedObjects++; - - if (processedObjects === objects.length) { - finishProcessing(); - } - } else { - rowData = rowData.concat(historicalData.slice(index, index + self.batchSize) - .map(self.table.getRowValues.bind(self.table, object, limitEvaluator))); - /* - Use timeout to yield process to other UI activities. On - return, process next batch - */ - self.timeoutHandle = self.$timeout(function () { - processData(object, historicalData, index + self.batchSize, limitEvaluator); - }); - } - } - - function makeTableRows(object, historicalData) { - // Only process the most recent request - if (requestTime === self.lastRequestTime) { - var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData(object, historicalData, 0, limitEvaluator); - } else { - resolve(rowData); - } - } - - /* - Use the telemetry API to request telemetry for a given object - */ - function requestData(object) { - return openmct.telemetry.request(object, { - start: bounds.start, - end: bounds.end - }).then(makeTableRows.bind(undefined, object)) - .catch(reject); - } - this.$timeout.cancel(this.timeoutHandle); - - if (objects.length > 0) { - objects.forEach(requestData); - } else { - self.loading(false); - resolve([]); - } - }.bind(this)); - - return promise; - }; - - - /** - * Subscribe to real-time data for the given objects. - * @private - * @param {object[]} objects The objects to subscribe to. - */ - TelemetryTableController.prototype.subscribeToNewData = function (objects) { - var telemetryApi = this.openmct.telemetry; - var telemetryCollection = this.telemetry; - //Set table max length to avoid unbounded growth. - var limitEvaluator; - var table = this.table; - - this.subscriptions.forEach(function (subscription) { - subscription(); - }); - this.subscriptions = []; - - function newData(domainObject, datum) { - limitEvaluator = telemetryApi.limitEvaluator(domainObject); - telemetryCollection.add([table.getRowValues(domainObject, limitEvaluator, datum)]); - } - - objects.forEach(function (object) { - this.subscriptions.push( - telemetryApi.subscribe(object, newData.bind(this, object), {})); - }.bind(this)); - - return objects; - }; - - /** - * Return an array of telemetry objects in this view that should be - * subscribed to. - * @private - * @returns {Promise} a promise that resolves with an array of - * telemetry objects in this view. - */ - TelemetryTableController.prototype.getTelemetryObjects = function () { - var telemetryApi = this.openmct.telemetry; - var compositionApi = this.openmct.composition; - - function filterForTelemetry(objects) { - return objects.filter(telemetryApi.isTelemetryObject.bind(telemetryApi)); - } - - /* - * If parent object is a telemetry object, subscribe to it. Do not - * test composees. - */ - if (telemetryApi.isTelemetryObject(this.domainObject)) { - return Promise.resolve([this.domainObject]); - } else { - /* - * If parent object is not a telemetry object, subscribe to all - * composees that are telemetry producing objects. - */ - var composition = compositionApi.get(this.domainObject); - - if (composition) { - return composition - .load() - .then(filterForTelemetry); - } - } - }; - - /** - * Request historical data, and subscribe to for real-time data. - * @private - * @returns {Promise} A promise that is resolved once subscription is - * established, and historical telemetry is received and processed. - */ - TelemetryTableController.prototype.getData = function () { - var scope = this.$scope; - - this.telemetry.clear(); - this.telemetry.bounds(this.openmct.time.bounds()); - - this.loading(true); - scope.rows = []; - - return this.getTelemetryObjects() - .then(this.loadColumns) - .then(this.subscribeToNewData) - .then(this.getHistoricalData) - .catch(function error(e) { - this.loading(false); - console.error(e.stack || e); - }.bind(this)); - }; - - TelemetryTableController.prototype.loading = function (loading) { - this.$timeout(function () { - this.$scope.loading = loading; - }.bind(this)); - }; - - /** - * When column configuration changes, update the visible headers - * accordingly. - * @private - */ - TelemetryTableController.prototype.filterColumns = function () { - var columnConfig = this.table.buildColumnConfiguration(); - - //Populate headers with visible columns (determined by configuration) - this.$scope.headers = Object.keys(columnConfig).filter(function (column) { - return columnConfig[column]; - }); - }; - - return TelemetryTableController; - } -); diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js deleted file mode 100644 index 2ce6d8d8f0..0000000000 --- a/platform/features/table/src/directives/MCTTable.js +++ /dev/null @@ -1,115 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../controllers/MCTTableController", - "../../res/templates/mct-table.html" - ], - function (MCTTableController, TableTemplate) { - /** - * 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() { - return { - restrict: "E", - template: TableTemplate, - controller: [ - '$scope', - '$window', - '$element', - 'exportService', - 'formatService', - 'openmct', - MCTTableController - ], - controllerAs: "table", - scope: { - headers: "=", - rows: "=", - formatCell: "=?", - enableFilter: "=?", - enableSort: "=?", - autoScroll: "=?", - // Used to indicate which columns contain time data. This - // will be used for determining when the table is sorted - // by the column that can be used for time conductor - // time of interest. - timeColumns: "=?", - // Indicate a column to sort on. Allows control of sort - // via configuration (eg. for default sort column). - defaultSort: "=?" - } - }; - } - - return MCTTable; - } -); diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js deleted file mode 100644 index 21402471df..0000000000 --- a/platform/features/table/test/TableConfigurationSpec.js +++ /dev/null @@ -1,214 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../src/TableConfiguration" - ], - function (Table) { - - describe("A table", function () { - var mockTableObject, - mockTelemetryObject, - mockAPI, - mockTelemetryAPI, - table, - mockTimeAPI, - mockObjectsAPI, - mockModel; - - beforeEach(function () { - mockTableObject = jasmine.createSpyObj('domainObject', - ['getModel', 'useCapability', 'getCapability', 'hasCapability'] - ); - mockModel = {}; - mockTableObject.getModel.and.returnValue(mockModel); - mockTableObject.getCapability.and.callFake(function (name) { - return name === 'editor' && { - isEditContextRoot: function () { - return true; - } - }; - }); - mockTelemetryObject = { - identifier: { - namespace: 'mock', - key: 'domainObject' - } - }; - - mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ - 'getValueFormatter' - ]); - mockTimeAPI = jasmine.createSpyObj('timeAPI', [ - 'timeSystem' - ]); - mockObjectsAPI = jasmine.createSpyObj('objectsAPI', [ - 'makeKeyString' - ]); - mockObjectsAPI.makeKeyString.and.callFake(function (identifier) { - return [identifier.namespace, identifier.key].join(':'); - }); - - mockAPI = { - telemetry: mockTelemetryAPI, - time: mockTimeAPI, - objects: mockObjectsAPI - }; - mockTelemetryAPI.getValueFormatter.and.callFake(function (metadata) { - var formatter = jasmine.createSpyObj( - 'telemetryFormatter:' + metadata.key, - [ - 'format', - 'parse' - ] - ); - var getter = function (datum) { - return datum[metadata.key]; - }; - formatter.format.and.callFake(getter); - formatter.parse.and.callFake(getter); - return formatter; - }); - - table = new Table(mockTableObject, mockAPI); - }); - - describe("Building columns from telemetry metadata", function () { - var metadata = [ - { - name: 'Range 1', - key: 'range1', - source: 'range1', - hints: { - range: 1 - } - }, - { - name: 'Range 2', - key: 'range2', - source: 'range2', - hints: { - range: 2 - } - }, - { - name: 'Domain 1', - key: 'domain1', - source: 'domain1', - format: 'utc', - hints: { - domain: 1 - } - }, - { - name: 'Domain 2', - key: 'domain2', - source: 'domain2', - format: 'utc', - hints: { - domain: 2 - } - } - ]; - - beforeEach(function () { - mockTimeAPI.timeSystem.and.returnValue({ - key: 'domain1' - }); - metadata.forEach(function (metadatum) { - table.addColumn(mockTelemetryObject, metadatum); - }); - }); - - it("populates columns", function () { - expect(table.columns.length).toBe(4); - }); - - it("Produces headers for each column based on metadata name", function () { - expect(table.headers.size).toBe(4); - Array.from(table.headers.values).forEach(function (header, i) { - expect(header).toEqual(metadata[i].name); - }); - }); - - it("Provides a default configuration with all columns" + - " visible", function () { - var configuration = table.buildColumnConfiguration(); - - 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.buildColumnConfiguration(); - - expect(tableConfig).toBeDefined(); - expect(tableConfig['Range 1']).toBe(false); - }); - - describe('retrieving row values', function () { - var datum, - rowValues; - - beforeEach(function () { - datum = { - 'range1': 10, - 'range2': 20, - 'domain1': 0, - 'domain2': 1 - }; - var limitEvaluator = { - evaluate: function () { - return { - "cssClass": "alarm-class" - }; - } - }; - rowValues = table.getRowValues(mockTelemetryObject, limitEvaluator, datum); - }); - - it("Returns a value for every column", function () { - expect(rowValues['Range 1'].text).toEqual(10); - }); - - it("Applies appropriate css class if limit violated.", function () { - expect(rowValues['Range 1'].cssClass).toEqual("alarm-class"); - }); - - }); - }); - }); - } -); diff --git a/platform/features/table/test/TelemetryCollectionSpec.js b/platform/features/table/test/TelemetryCollectionSpec.js deleted file mode 100644 index 6738ebefff..0000000000 --- a/platform/features/table/test/TelemetryCollectionSpec.js +++ /dev/null @@ -1,212 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../src/TelemetryCollection" - ], - function (TelemetryCollection) { - - describe("A telemetry collection", function () { - - var collection; - var telemetryObjects; - var ms; - var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", - "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"]; - - beforeEach(function () { - telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) { - ms = number * 1000; - return { - timestamp: ms, - value: { - integer: number, - text: integerTextMap[number] - } - }; - }); - collection = new TelemetryCollection(); - }); - - it("Sorts inserted telemetry by specified field", - function () { - collection.sort('value.integer'); - collection.add(telemetryObjects); - expect(collection.telemetry[0].value.integer).toBe(0); - expect(collection.telemetry[1].value.integer).toBe(1); - expect(collection.telemetry[2].value.integer).toBe(2); - expect(collection.telemetry[3].value.integer).toBe(3); - - collection.sort('value.text'); - expect(collection.telemetry[0].value.text).toBe("EIGHT"); - expect(collection.telemetry[1].value.text).toBe("FIVE"); - expect(collection.telemetry[2].value.text).toBe("FOUR"); - expect(collection.telemetry[3].value.text).toBe("NINE"); - } - ); - - describe("on bounds change", function () { - var discardedCallback; - - beforeEach(function () { - discardedCallback = jasmine.createSpy("discarded"); - collection.on("discarded", discardedCallback); - collection.sort("timestamp"); - collection.add(telemetryObjects); - collection.bounds({start: 5000, end: 8000}); - }); - - - it("emits an event indicating that telemetry has " + - "been discarded", function () { - expect(discardedCallback).toHaveBeenCalled(); - }); - - it("discards telemetry data with a time stamp " + - "before specified start bound", function () { - var discarded = discardedCallback.calls.mostRecent().args[0]; - - // Expect 5 because as an optimization, the TelemetryCollection - // will not consider telemetry values that exceed the upper - // bounds. Arbitrary bounds changes in which the end bound is - // decreased is assumed to require a new historical query, and - // hence re-population of the collection anyway - expect(discarded.length).toBe(5); - expect(discarded[0].value.integer).toBe(0); - expect(discarded[1].value.integer).toBe(1); - expect(discarded[4].value.integer).toBe(4); - }); - }); - - describe("when adding telemetry to a collection", function () { - var addedCallback; - beforeEach(function () { - collection.sort("timestamp"); - collection.add(telemetryObjects); - addedCallback = jasmine.createSpy("added"); - collection.on("added", addedCallback); - }); - - it("emits an event", - function () { - var addedObject = { - timestamp: 10000, - value: { - integer: 10, - text: integerTextMap[10] - } - }; - collection.add([addedObject]); - expect(addedCallback).toHaveBeenCalledWith([addedObject]); - } - ); - it("inserts in the correct order", - function () { - var addedObjectA = { - timestamp: 10000, - value: { - integer: 10, - text: integerTextMap[10] - } - }; - var addedObjectB = { - timestamp: 11000, - value: { - integer: 11, - text: integerTextMap[11] - } - }; - collection.add([addedObjectB, addedObjectA]); - - expect(collection.telemetry[11]).toBe(addedObjectB); - } - ); - it("maintains insertion order in the case of duplicate time stamps", - function () { - var addedObjectA = { - timestamp: 10000, - value: { - integer: 10, - text: integerTextMap[10] - } - }; - var addedObjectB = { - timestamp: 10000, - value: { - integer: 11, - text: integerTextMap[11] - } - }; - collection.add([addedObjectA, addedObjectB]); - - expect(collection.telemetry[11]).toBe(addedObjectB); - } - ); - }); - - describe("buffers telemetry", function () { - var addedObjectA; - var addedObjectB; - - beforeEach(function () { - collection.sort("timestamp"); - collection.add(telemetryObjects); - - addedObjectA = { - timestamp: 10000, - value: { - integer: 10, - text: integerTextMap[10] - } - }; - addedObjectB = { - timestamp: 11000, - value: { - integer: 11, - text: integerTextMap[11] - } - }; - - collection.bounds({start: 0, end: 10000}); - collection.add([addedObjectA, addedObjectB]); - }); - it("when it falls outside of bounds", function () { - expect(collection.highBuffer).toBeDefined(); - expect(collection.highBuffer.length).toBe(1); - expect(collection.highBuffer[0]).toBe(addedObjectB); - }); - it("and adds it to collection when it falls within bounds", function () { - expect(collection.telemetry.length).toBe(11); - collection.bounds({start: 0, end: 11000}); - expect(collection.telemetry.length).toBe(12); - expect(collection.telemetry[11]).toBe(addedObjectB); - }); - it("and removes it from the buffer when it falls within bounds", function () { - expect(collection.highBuffer.length).toBe(1); - collection.bounds({start: 0, end: 11000}); - expect(collection.highBuffer.length).toBe(0); - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js deleted file mode 100644 index f004bd69fa..0000000000 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ /dev/null @@ -1,598 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "zepto", - "moment", - "../../src/controllers/MCTTableController" - ], - function ($, moment, MCTTableController) { - - var MOCK_ELEMENT_TEMPLATE = - '
' + - '
' + - '
' + - '
'; - - describe('The MCTTable Controller', function () { - - var controller, - mockScope, - watches, - mockWindow, - mockElement, - mockExportService, - mockConductor, - mockFormatService, - mockFormat; - - function getCallback(target, event) { - return target.calls.all().filter(function (call) { - return call.args[0] === event; - })[0].args[1]; - } - - beforeEach(function () { - watches = {}; - - mockScope = jasmine.createSpyObj('scope', [ - '$watch', - '$on', - '$watchCollection', - '$digest' - ]); - mockScope.$watchCollection.and.callFake(function (event, callback) { - watches[event] = callback; - }); - - mockElement = $(MOCK_ELEMENT_TEMPLATE); - mockExportService = jasmine.createSpyObj('exportService', [ - 'exportCSV' - ]); - - mockConductor = jasmine.createSpyObj('conductor', [ - 'bounds', - 'timeOfInterest', - 'timeSystem', - 'on', - 'off' - ]); - - mockScope.displayHeaders = true; - mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']); - mockWindow.requestAnimationFrame.and.callFake(function (f) { - return f(); - }); - - mockFormat = jasmine.createSpyObj('formatter', [ - 'parse', - 'format' - ]); - mockFormatService = jasmine.createSpyObj('formatService', [ - 'getFormat' - ]); - mockFormatService.getFormat.and.returnValue(mockFormat); - - controller = new MCTTableController( - mockScope, - mockWindow, - mockElement, - mockExportService, - mockFormatService, - {time: mockConductor} - ); - spyOn(controller, 'setVisibleRows').and.callThrough(); - }); - - it('Reacts to changes to filters, headers, and rows', function () { - expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function)); - expect(mockScope.$watch).toHaveBeenCalledWith('headers', jasmine.any(Function)); - expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function)); - }); - - it('unregisters listeners on destruction', function () { - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); - getCallback(mockScope.$on, '$destroy')(); - - expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem); - expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest); - expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds); - }); - - describe('The time of interest', function () { - var rowsAsc = []; - var rowsDesc = []; - beforeEach(function () { - rowsAsc = [ - { - 'col1': {'text': 'row1 col1 match'}, - 'col2': {'text': '2012-10-31 00:00:00.000Z'}, - 'col3': {'text': 'row1 col3'} - }, - { - 'col1': {'text': 'row2 col1 match'}, - 'col2': {'text': '2012-11-01 00:00:00.000Z'}, - 'col3': {'text': 'row2 col3'} - }, - { - 'col1': {'text': 'row3 col1'}, - 'col2': {'text': '2012-11-03 00:00:00.000Z'}, - 'col3': {'text': 'row3 col3'} - }, - { - 'col1': {'text': 'row3 col1'}, - 'col2': {'text': '2012-11-04 00:00:00.000Z'}, - 'col3': {'text': 'row3 col3'} - } - ]; - rowsDesc = [ - { - 'col1': {'text': 'row1 col1 match'}, - 'col2': {'text': '2012-11-02 00:00:00.000Z'}, - 'col3': {'text': 'row1 col3'} - }, - { - 'col1': {'text': 'row2 col1 match'}, - 'col2': {'text': '2012-11-01 00:00:00.000Z'}, - 'col3': {'text': 'row2 col3'} - }, - { - 'col1': {'text': 'row3 col1'}, - 'col2': {'text': '2012-10-30 00:00:00.000Z'}, - 'col3': {'text': 'row3 col3'} - }, - { - 'col1': {'text': 'row3 col1'}, - 'col2': {'text': '2012-10-29 00:00:00.000Z'}, - 'col3': {'text': 'row3 col3'} - } - ]; - mockScope.timeColumns = ['col2']; - mockScope.sortColumn = 'col2'; - controller.toiFormatter = mockFormat; - }); - it("is observed for changes", function () { - //Mock setting time columns - getCallback(mockScope.$watch, 'timeColumns')(['col2']); - - expect(mockConductor.on).toHaveBeenCalledWith('timeOfInterest', - jasmine.any(Function)); - }); - describe("causes corresponding row to be highlighted", function () { - it("when changed and rows sorted ascending", function () { - var testDate = "2012-11-02 00:00:00.000Z"; - mockScope.rows = rowsAsc; - mockScope.displayRows = rowsAsc; - mockScope.sortDirection = 'asc'; - - var toi = moment.utc(testDate).valueOf(); - mockFormat.parse.and.returnValue(toi); - mockFormat.format.and.returnValue(testDate); - - //mock setting the timeColumns parameter - getCallback(mockScope.$watch, 'timeColumns')(['col2']); - - var toiCallback = getCallback(mockConductor.on, 'timeOfInterest'); - toiCallback(toi); - - expect(mockScope.toiRowIndex).toBe(2); - }); - it("when changed and rows sorted descending", function () { - var testDate = "2012-10-31 00:00:00.000Z"; - mockScope.rows = rowsDesc; - mockScope.displayRows = rowsDesc; - mockScope.sortDirection = 'desc'; - - var toi = moment.utc(testDate).valueOf(); - mockFormat.parse.and.returnValue(toi); - mockFormat.format.and.returnValue(testDate); - - //mock setting the timeColumns parameter - getCallback(mockScope.$watch, 'timeColumns')(['col2']); - - var toiCallback = getCallback(mockConductor.on, 'timeOfInterest'); - toiCallback(toi); - - expect(mockScope.toiRowIndex).toBe(2); - }); - it("when rows are set and sorted ascending", function () { - var testDate = "2012-11-02 00:00:00.000Z"; - mockScope.sortDirection = 'asc'; - - var toi = moment.utc(testDate).valueOf(); - mockFormat.parse.and.returnValue(toi); - mockFormat.format.and.returnValue(testDate); - mockConductor.timeOfInterest.and.returnValue(toi); - - //mock setting the timeColumns parameter - getCallback(mockScope.$watch, 'timeColumns')(['col2']); - - //Mock setting the rows on scope - var rowsCallback = getCallback(mockScope.$watch, 'rows'); - var setRowsPromise = rowsCallback(rowsAsc); - - return setRowsPromise.then(function () { - expect(mockScope.toiRowIndex).toBe(2); - }); - }); - - }); - }); - - 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'} - } - ]; - mockScope.rows = testRows; - }); - - 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.setRows(testRows); - expect(mockScope.displayRows.length).toBe(3); - expect(mockScope.displayRows).toEqual(testRows); - }); - - it('Supports adding rows individually', function () { - var addRowFunc = getCallback(mockScope.$on, 'add:rows'), - row4 = { - 'col1': {'text': 'row3 col1'}, - 'col2': {'text': 'ghi'}, - 'col3': {'text': 'row3 col3'} - }; - controller.setRows(testRows); - expect(mockScope.displayRows.length).toBe(3); - testRows.push(row4); - addRowFunc(undefined, [row4]); - expect(mockScope.displayRows.length).toBe(4); - }); - - it('Supports removing rows individually', function () { - var removeRowFunc = getCallback(mockScope.$on, 'remove:rows'); - controller.setRows(testRows); - expect(mockScope.displayRows.length).toBe(3); - removeRowFunc(undefined, [testRows[2]]); - expect(mockScope.displayRows.length).toBe(2); - expect(controller.setVisibleRows).toHaveBeenCalled(); - }); - - it("can be exported as CSV", function () { - controller.setRows(testRows); - controller.setHeaders(Object.keys(testRows[0])); - mockScope.exportAsCSV(); - expect(mockExportService.exportCSV) - .toHaveBeenCalled(); - mockExportService.exportCSV.calls.mostRecent().args[0] - .forEach(function (row, i) { - Object.keys(row).forEach(function (k) { - expect(row[k]).toEqual( - mockScope.displayRows[i][k].text - ); - }); - }); - }); - - describe('sorting', function () { - var sortedRows; - - beforeEach(function () { - 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'); - }); - - it('Allows sort column to be changed externally by ' + - 'setting or changing sortBy attribute', function () { - mockScope.displayRows = testRows; - var sortByCB = getCallback(mockScope.$watch, 'defaultSort'); - sortByCB('col2'); - - expect(mockScope.sortDirection).toEqual('asc'); - - expect(mockScope.displayRows[0].col2.text).toEqual('abc'); - expect(mockScope.displayRows[1].col2.text).toEqual('def'); - expect(mockScope.displayRows[2].col2.text).toEqual('ghi'); - - }); - - // https://github.com/nasa/openmct/issues/910 - it('updates visible rows in scope', function () { - var oldRows; - mockScope.rows = testRows; - var setRowsPromise = controller.setRows(testRows); - - oldRows = mockScope.visibleRows; - mockScope.toggleSort('col2'); - - return setRowsPromise.then(function () { - expect(mockScope.visibleRows).not.toEqual(oldRows); - }); - }); - - 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 () { - var row4, - row5, - row6; - - beforeEach(function () { - row4 = { - 'col1': {'text': 'row4 col1'}, - 'col2': {'text': 'xyz'}, - 'col3': {'text': 'row4 col3'} - }; - row5 = { - 'col1': {'text': 'row5 col1'}, - 'col2': {'text': 'aaa'}, - 'col3': {'text': 'row5 col3'} - }; - row6 = { - 'col1': {'text': 'row6 col1'}, - 'col2': {'text': 'ggg'}, - 'col3': {'text': 'row6 col3'} - }; - }); - - it('Adds new rows at the correct sort position when' + - ' sorted ', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'desc'; - - mockScope.displayRows = controller.sortRows(testRows.slice(0)); - - controller.addRows(undefined, [row4, row5, row6, row6]); - expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); - expect(mockScope.displayRows[6].col2.text).toEqual('aaa'); - //Added a duplicate row - expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); - expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); - }); - - it('Inserts duplicate values for sort column in order received when sorted descending', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'desc'; - - mockScope.displayRows = controller.sortRows(testRows.slice(0)); - - var row6b = { - 'col1': {'text': 'row6b col1'}, - 'col2': {'text': 'ggg'}, - 'col3': {'text': 'row6b col3'} - }; - var row6c = { - 'col1': {'text': 'row6c col1'}, - 'col2': {'text': 'ggg'}, - 'col3': {'text': 'row6c col3'} - }; - - controller.addRows(undefined, [row4, row5]); - controller.addRows(undefined, [row6, row6b, row6c]); - expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); - expect(mockScope.displayRows[7].col2.text).toEqual('aaa'); - - // Added duplicate rows - expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); - expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); - expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); - - // Check that original order is maintained with dupes - expect(mockScope.displayRows[2].col3.text).toEqual('row6c col3'); - expect(mockScope.displayRows[3].col3.text).toEqual('row6b col3'); - expect(mockScope.displayRows[4].col3.text).toEqual('row6 col3'); - }); - - it('Inserts duplicate values for sort column in order received when sorted ascending', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'asc'; - - mockScope.displayRows = controller.sortRows(testRows.slice(0)); - - var row6b = { - 'col1': {'text': 'row6b col1'}, - 'col2': {'text': 'ggg'}, - 'col3': {'text': 'row6b col3'} - }; - var row6c = { - 'col1': {'text': 'row6c col1'}, - 'col2': {'text': 'ggg'}, - 'col3': {'text': 'row6c col3'} - }; - - controller.addRows(undefined, [row4, row5, row6]); - controller.addRows(undefined, [row6b, row6c]); - expect(mockScope.displayRows[0].col2.text).toEqual('aaa'); - expect(mockScope.displayRows[7].col2.text).toEqual('xyz'); - - // Added duplicate rows - expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); - expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); - expect(mockScope.displayRows[5].col2.text).toEqual('ggg'); - // Check that original order is maintained with dupes - expect(mockScope.displayRows[3].col3.text).toEqual('row6 col3'); - expect(mockScope.displayRows[4].col3.text).toEqual('row6b col3'); - expect(mockScope.displayRows[5].col3.text).toEqual('row6c col3'); - }); - - it('Adds new rows at the correct sort position when' + - ' sorted and filtered', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'desc'; - mockScope.filters = {'col2': 'a'};//Include only - // rows with 'a' - - mockScope.displayRows = controller.sortRows(testRows.slice(0)); - mockScope.displayRows = controller.filterRows(testRows); - - controller.addRows(undefined, [row5]); - expect(mockScope.displayRows.length).toBe(2); - expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); - - controller.addRows(undefined, [row6]); - 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 () { - mockScope.sortColumn = undefined; - mockScope.sortDirection = undefined; - mockScope.filters = {}; - - mockScope.displayRows = testRows.slice(0); - - controller.addRows(undefined, [row5]); - expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); - - controller.addRows(undefined, [row6]); - 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); - - controller.addRows(undefined, [row7]); - expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); - }); - - }); - }); - }); - }); - }); diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js deleted file mode 100644 index f88b26a428..0000000000 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ /dev/null @@ -1,113 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../../src/controllers/TableOptionsController" - ], - function (TableOptionsController) { - - describe('The Table Options Controller', function () { - var mockDomainObject, - mockCapability, - controller, - mockScope; - - beforeEach(function () { - mockCapability = jasmine.createSpyObj('mutationCapability', [ - 'listen' - ]); - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'getModel' - ]); - mockDomainObject.getCapability.and.returnValue(mockCapability); - mockDomainObject.getModel.and.returnValue({}); - - mockScope = jasmine.createSpyObj('scope', [ - '$watchCollection', - '$watch', - '$on' - ]); - 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.calls.mostRecent().args[1](); - expect(unlistenFunc).toHaveBeenCalled(); - }); - - it('Registers a listener for mutation events on the object', function () { - mockScope.$watch.calls.mostRecent().args[1](mockDomainObject); - 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); - }); - }); - }); - - }); diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js deleted file mode 100644 index bb52d3153f..0000000000 --- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js +++ /dev/null @@ -1,417 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - '../../src/controllers/TelemetryTableController', - '../../../../../src/api/objects/object-utils', - 'lodash' - ], - function (TelemetryTableController, objectUtils, _) { - - describe('The TelemetryTableController', function () { - - var controller, - mockScope, - mockTimeout, - mockConductor, - mockAPI, - mockDomainObject, - mockTelemetryAPI, - mockObjectAPI, - mockCompositionAPI, - unobserve, - mockBounds; - - function getCallback(target, event) { - return target.calls.all().filter(function (call) { - return call.args[0] === event; - })[0].args[1]; - } - - beforeEach(function () { - mockBounds = { - start: 0, - end: 10 - }; - mockConductor = jasmine.createSpyObj("conductor", [ - "bounds", - "clock", - "on", - "off", - "timeSystem" - ]); - mockConductor.bounds.and.returnValue(mockBounds); - mockConductor.clock.and.returnValue(undefined); - - mockDomainObject = jasmine.createSpyObj("domainObject", [ - "getModel", - "getId", - "useCapability", - "hasCapability" - ]); - mockDomainObject.getModel.and.returnValue({}); - mockDomainObject.getId.and.returnValue("mockId"); - mockDomainObject.useCapability.and.returnValue(true); - - mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [ - "get" - ]); - - mockObjectAPI = jasmine.createSpyObj("objectAPI", [ - "observe", - "makeKeyString" - ]); - unobserve = jasmine.createSpy("unobserve"); - mockObjectAPI.observe.and.returnValue(unobserve); - - mockScope = jasmine.createSpyObj("scope", [ - "$on", - "$watch", - "$broadcast" - ]); - mockScope.domainObject = mockDomainObject; - - mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [ - "isTelemetryObject", - "subscribe", - "getMetadata", - "commonValuesForHints", - "request", - "limitEvaluator", - "getValueFormatter" - ]); - mockTelemetryAPI.commonValuesForHints.and.returnValue([]); - mockTelemetryAPI.request.and.returnValue(Promise.resolve([])); - mockTelemetryAPI.getMetadata.and.returnValue({ - values: function () { - return []; - } - }); - mockTelemetryAPI.getValueFormatter.and.callFake(function (metadata) { - var formatter = jasmine.createSpyObj( - 'telemetryFormatter:' + metadata.key, - [ - 'format', - 'parse' - ] - ); - var getter = function (datum) { - return datum[metadata.key]; - }; - formatter.format.and.callFake(getter); - formatter.parse.and.callFake(getter); - return formatter; - }); - - mockTelemetryAPI.isTelemetryObject.and.returnValue(false); - - mockTimeout = jasmine.createSpy("timeout"); - mockTimeout.and.returnValue(1); // Return something - mockTimeout.cancel = jasmine.createSpy("cancel"); - - mockAPI = { - time: mockConductor, - objects: mockObjectAPI, - telemetry: mockTelemetryAPI, - composition: mockCompositionAPI - }; - controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI); - }); - - describe('listens for', function () { - beforeEach(function () { - controller.registerChangeListeners(); - }); - it('object mutation', function () { - var calledObject = mockObjectAPI.observe.calls.mostRecent().args[0]; - - expect(mockObjectAPI.observe).toHaveBeenCalled(); - expect(calledObject.identifier.key).toEqual(mockDomainObject.getId()); - }); - it('conductor changes', function () { - expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); - expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); - expect(mockConductor.on).toHaveBeenCalledWith("clock", jasmine.any(Function)); - }); - }); - - describe('deregisters all listeners on scope destruction', function () { - var timeSystemListener, - boundsListener, - clockListener; - - beforeEach(function () { - controller.registerChangeListeners(); - - timeSystemListener = getCallback(mockConductor.on, "timeSystem"); - boundsListener = getCallback(mockConductor.on, "bounds"); - clockListener = getCallback(mockConductor.on, "clock"); - - var destroy = getCallback(mockScope.$on, "$destroy"); - destroy(); - }); - - it('object mutation', function () { - expect(unobserve).toHaveBeenCalled(); - }); - it('conductor changes', function () { - expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener); - expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener); - expect(mockConductor.off).toHaveBeenCalledWith("clock", clockListener); - }); - }); - - describe ('when getting telemetry', function () { - var mockComposition, - mockTelemetryObject, - mockChildren, - unsubscribe; - - beforeEach(function () { - mockComposition = jasmine.createSpyObj("composition", [ - "load" - ]); - - mockTelemetryObject = {}; - mockTelemetryObject.identifier = { - key: "mockTelemetryObject" - }; - - unsubscribe = jasmine.createSpy("unsubscribe"); - mockTelemetryAPI.subscribe.and.returnValue(unsubscribe); - - mockChildren = [mockTelemetryObject]; - mockComposition.load.and.returnValue(Promise.resolve(mockChildren)); - mockCompositionAPI.get.and.returnValue(mockComposition); - - mockTelemetryAPI.isTelemetryObject.and.callFake(function (obj) { - return obj.identifier.key === mockTelemetryObject.identifier.key; - }); - }); - - it('fetches historical data for the time period specified by the conductor bounds', function () { - return controller.getData().then(function () { - expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds); - }); - }); - - it('unsubscribes on view destruction', function () { - return controller.getData().then(function () { - var destroy = getCallback(mockScope.$on, "$destroy"); - destroy(); - - expect(unsubscribe).toHaveBeenCalled(); - }); - }); - it('fetches historical data for the time period specified by the conductor bounds', function () { - return controller.getData().then(function () { - expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds); - }); - }); - - it('fetches data for, and subscribes to parent object if it is a telemetry object', function () { - return controller.getData().then(function () { - expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {}); - expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object)); - }); - }); - it('fetches data for, and subscribes to parent object if it is a telemetry object', function () { - return controller.getData().then(function () { - expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {}); - expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object)); - }); - }); - - it('fetches data for, and subscribes to any composees that are telemetry objects if parent is not', function () { - mockChildren = [ - {name: "child 1"} - ]; - var mockTelemetryChildren = [ - {name: "child 2"}, - {name: "child 3"}, - {name: "child 4"} - ]; - mockChildren = mockChildren.concat(mockTelemetryChildren); - mockComposition.load.and.returnValue(Promise.resolve(mockChildren)); - - mockTelemetryAPI.isTelemetryObject.and.callFake(function (object) { - if (object === mockTelemetryObject) { - return false; - } else { - return mockTelemetryChildren.indexOf(object) !== -1; - } - }); - - return controller.getData().then(function () { - mockTelemetryChildren.forEach(function (child) { - expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(child, jasmine.any(Function), {}); - }); - - mockTelemetryChildren.forEach(function (child) { - expect(mockTelemetryAPI.request).toHaveBeenCalledWith(child, jasmine.any(Object)); - }); - - expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockChildren[0], jasmine.any(Function), {}); - expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockTelemetryObject[0], jasmine.any(Function), {}); - }); - }); - }); - - it('When in real-time mode, enables auto-scroll', function () { - controller.registerChangeListeners(); - - var clockCallback = getCallback(mockConductor.on, "clock"); - //Confirm pre-condition - expect(mockScope.autoScroll).toBeFalsy(); - - //Mock setting the a clock in the Time API - clockCallback({}); - expect(mockScope.autoScroll).toBe(true); - }); - - describe('populates table columns', function () { - var allMetadata; - var mockTimeSystem1; - var mockTimeSystem2; - - beforeEach(function () { - allMetadata = [{ - key: "column1", - name: "Column 1", - hints: { - domain: 1 - } - }, { - key: "column2", - name: "Column 2", - hints: { - domain: 2 - } - }, { - key: "column3", - name: "Column 3", - hints: {} - }]; - - mockTimeSystem1 = { - key: "column1" - }; - mockTimeSystem2 = { - key: "column2" - }; - - mockConductor.timeSystem.and.returnValue(mockTimeSystem1); - - mockTelemetryAPI.getMetadata.and.returnValue({ - values: function () { - return allMetadata; - } - }); - - controller.loadColumns([mockDomainObject]); - }); - - it('based on metadata for given objects', function () { - expect(mockScope.headers).toBeDefined(); - expect(mockScope.headers.length).toBeGreaterThan(0); - expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1); - expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1); - expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1); - }); - - it('and sorts by column matching time system', function () { - expect(mockScope.defaultSort).toEqual("Column 1"); - - mockConductor.timeSystem.and.returnValue(mockTimeSystem2); - controller.sortByTimeSystem(); - - expect(mockScope.defaultSort).toEqual("Column 2"); - }); - - it('batches processing of rows for performance when receiving historical telemetry', function () { - var mockHistoricalData = [ - { - "column1": 1, - "column2": 2, - "column3": 3 - },{ - "column1": 4, - "column2": 5, - "column3": 6 - }, { - "column1": 7, - "column2": 8, - "column3": 9 - } - ]; - - controller.batchSize = 2; - mockTelemetryAPI.request.and.returnValue(Promise.resolve(mockHistoricalData)); - controller.getHistoricalData([mockDomainObject]); - - return new Promise(function (resolve) { - mockTimeout.and.callFake(function () { - resolve(); - }); - }).then(function () { - mockTimeout.calls.mostRecent().args[0](); - expect(mockTimeout.calls.count()).toBe(2); - mockTimeout.calls.mostRecent().args[0](); - expect(mockScope.rows.length).toBe(3); - - }); - }); - }); - - it('Removes telemetry rows from table when they fall out of bounds', function () { - var discardedRows = [ - {"column1": "value 1"}, - {"column2": "value 2"}, - {"column3": "value 3"} - ]; - - spyOn(controller.telemetry, "on").and.callThrough(); - - controller.registerChangeListeners(); - expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function)); - var onDiscard = getCallback(controller.telemetry.on, "discarded"); - onDiscard(discardedRows); - expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows); - }); - - describe('when telemetry is added', function () { - var testRows; - - beforeEach(function () { - testRows = [{ a: 0 }, { a: 1 }, { a: 2 }]; - - controller.registerChangeListeners(); - controller.telemetry.add(testRows); - }); - - it("Adds the rows to the MCTTable directive", function () { - expect(mockScope.$broadcast).toHaveBeenCalledWith("add:rows", testRows); - }); - }); - }); - }); diff --git a/src/defaultRegistry.js b/src/defaultRegistry.js index a9d97ed564..c75cd3f0b2 100644 --- a/src/defaultRegistry.js +++ b/src/defaultRegistry.js @@ -64,7 +64,6 @@ define([ '../platform/features/pages/bundle', '../platform/features/hyperlink/bundle', '../platform/features/static-markup/bundle', - '../platform/features/table/bundle', '../platform/features/timeline/bundle', '../platform/forms/bundle', '../platform/framework/bundle', @@ -108,7 +107,6 @@ define([ 'platform/features/pages', 'platform/features/hyperlink', 'platform/features/timeline', - 'platform/features/table', 'platform/forms', 'platform/identity', 'platform/persistence/aggregator', diff --git a/src/exporters/CSVExporter.js b/src/exporters/CSVExporter.js new file mode 100644 index 0000000000..284d38fea3 --- /dev/null +++ b/src/exporters/CSVExporter.js @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + 'csv', + 'saveAs' +], function (CSV, saveAs) { + class CSVExporter { + export(rows, options) { + let headers = (options && options.headers) || + (Object.keys((rows[0] || {})).sort()); + let filename = (options && options.filename) || "export.csv"; + let csvText = new CSV(rows, { header: headers }).encode(); + let blob = new Blob([csvText], { type: "text/csv" }); + saveAs(blob, filename); + } + } + + return CSVExporter; +}); \ No newline at end of file diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index e7df3bfb7e..e404e8ebca 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -33,6 +33,7 @@ define([ './URLIndicatorPlugin/URLIndicatorPlugin', './telemetryMean/plugin', './plot/plugin', + './telemetryTable/plugin', './staticRootPlugin/plugin' ], function ( _, @@ -47,6 +48,7 @@ define([ URLIndicatorPlugin, TelemetryMean, PlotPlugin, + TelemetryTablePlugin, StaticRootPlugin ) { var bundleMap = { @@ -152,7 +154,8 @@ define([ plugins.ExampleImagery = ExampleImagery; plugins.Plot = PlotPlugin; - + plugins.TelemetryTable = TelemetryTablePlugin; + plugins.SummaryWidget = SummaryWidget; plugins.TelemetryMean = TelemetryMean; plugins.URLIndicator = URLIndicatorPlugin; diff --git a/src/plugins/telemetryTable/TableConfigurationComponent.js b/src/plugins/telemetryTable/TableConfigurationComponent.js new file mode 100644 index 0000000000..9f8f60892f --- /dev/null +++ b/src/plugins/telemetryTable/TableConfigurationComponent.js @@ -0,0 +1,87 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + define([ + 'lodash', + 'vue', + './table-configuration.html', + './TelemetryTableConfiguration' +],function ( + _, + Vue, + TableConfigurationTemplate, + TelemetryTableConfiguration +) { + return function TableConfigurationComponent(domainObject, openmct) { + const tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); + let unlisteners = []; + + return new Vue({ + template: TableConfigurationTemplate, + data() { + return { + headers: {}, + configuration: tableConfiguration.getConfiguration() + } + }, + methods: { + updateHeaders(headers) { + this.headers = headers; + }, + toggleColumn(key) { + let isHidden = this.configuration.hiddenColumns[key] === true; + + this.configuration.hiddenColumns[key] = !isHidden; + tableConfiguration.updateConfiguration(this.configuration); + }, + addObject(domainObject) { + tableConfiguration.addColumnsForObject(domainObject, true); + this.updateHeaders(tableConfiguration.getAllHeaders()); + }, + removeObject(objectIdentifier) { + tableConfiguration.removeColumnsForObject(objectIdentifier, true); + this.updateHeaders(tableConfiguration.getAllHeaders()); + } + + }, + mounted() { + let compositionCollection = openmct.composition.get(domainObject); + + compositionCollection.load() + .then((composition) => { + tableConfiguration.addColumnsForAllObjects(composition); + this.updateHeaders(tableConfiguration.getAllHeaders()); + + compositionCollection.on('add', this.addObject); + unlisteners.push(compositionCollection.off.bind(compositionCollection, 'add', this.addObject)); + + compositionCollection.on('remove', this.removeObject); + unlisteners.push(compositionCollection.off.bind(compositionCollection, 'remove', this.removeObject)); + }); + }, + destroyed() { + tableConfiguration.destroy(); + unlisteners.forEach((unlisten) => unlisten()); + } + }); + } + }); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TableConfigurationViewProvider.js b/src/plugins/telemetryTable/TableConfigurationViewProvider.js new file mode 100644 index 0000000000..1203160c6c --- /dev/null +++ b/src/plugins/telemetryTable/TableConfigurationViewProvider.js @@ -0,0 +1,85 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + '../../api/objects/object-utils', + './TableConfigurationComponent' +], function ( + objectUtils, + TableConfigurationComponent +) { + function TableConfigurationViewProvider(openmct) { + let instantiateService; + + function isBeingEdited(object) { + let oldStyleObject = getOldStyleObject(object); + + return oldStyleObject.hasCapability('editor') && + oldStyleObject.getCapability('editor').inEditContext(); + } + + function getOldStyleObject(object) { + let oldFormatModel = objectUtils.toOldFormat(object); + let oldFormatId = objectUtils.makeKeyString(object.identifier); + + return instantiate(oldFormatModel, oldFormatId); + } + + function instantiate(model, id) { + if (!instantiateService) { + instantiateService = openmct.$injector.get('instantiate'); + } + return instantiateService(model, id); + } + + return { + key: 'table-configuration', + name: 'Telemetry Table Configuration', + canView: function (selection) { + let object = selection[0].context.item; + + return selection.length > 0 && + object.type === 'table' && + isBeingEdited(object); + }, + view: function (selection) { + let component; + let domainObject = selection[0].context.item; + return { + show: function (element) { + component = TableConfigurationComponent(domainObject, openmct); + element.appendChild(component.$mount().$el); + }, + destroy: function (element) { + component.$destroy(); + element.removeChild(component.$el); + component = undefined; + } + } + }, + priority: function () { + return 1; + } + } + } + return TableConfigurationViewProvider; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js new file mode 100644 index 0000000000..92dc238e88 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -0,0 +1,201 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + 'EventEmitter', + 'lodash', + './collections/BoundedTableRowCollection', + './collections/FilteredTableRowCollection', + './TelemetryTableRow', + './TelemetryTableConfiguration' +], function ( + EventEmitter, + _, + BoundedTableRowCollection, + FilteredTableRowCollection, + TelemetryTableRow, + TelemetryTableConfiguration +) { + class TelemetryTable extends EventEmitter { + constructor(domainObject, rowCount, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + this.rowCount = rowCount; + this.subscriptions = {}; + this.tableComposition = undefined; + this.telemetryObjects = []; + this.outstandingRequests = 0; + this.configuration = new TelemetryTableConfiguration(domainObject, openmct); + + this.addTelemetryObject = this.addTelemetryObject.bind(this); + this.removeTelemetryObject = this.removeTelemetryObject.bind(this); + this.isTelemetryObject = this.isTelemetryObject.bind(this); + this.refreshData = this.refreshData.bind(this); + this.requestDataFor = this.requestDataFor.bind(this); + + this.createTableRowCollections(); + openmct.time.on('bounds', this.refreshData); + } + + initialize() { + if (this.domainObject.type === 'table') { + this.loadComposition(); + } else { + this.addTelemetryObject(this.domainObject); + } + } + + createTableRowCollections() { + this.boundedRows = new BoundedTableRowCollection(this.openmct); + + //By default, sort by current time system, ascending. + this.filteredRows = new FilteredTableRowCollection(this.boundedRows); + this.filteredRows.sortBy({ + key: this.openmct.time.timeSystem().key, + direction: 'asc' + }); + } + + loadComposition() { + this.tableComposition = this.openmct.composition.get(this.domainObject); + if (this.tableComposition !== undefined){ + this.tableComposition.load().then((composition)=>{ + composition = composition.filter(this.isTelemetryObject); + + this.configuration.addColumnsForAllObjects(composition); + composition.forEach(this.addTelemetryObject); + + this.tableComposition.on('add', this.addTelemetryObject); + this.tableComposition.on('remove', this.removeTelemetryObject); + }); + } + } + + addTelemetryObject(telemetryObject) { + this.configuration.addColumnsForObject(telemetryObject, true); + this.requestDataFor(telemetryObject); + this.subscribeTo(telemetryObject); + this.telemetryObjects.push(telemetryObject); + + this.emit('object-added', telemetryObject); + } + + removeTelemetryObject(objectIdentifier) { + this.configuration.removeColumnsForObject(objectIdentifier, true); + let keyString = this.openmct.objects.makeKeyString(objectIdentifier); + this.boundedRows.removeAllRowsForObject(keyString); + this.unsubscribe(keyString); + this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(objectIdentifier, object.identifier)); + + this.emit('object-removed', objectIdentifier); + } + + requestDataFor(telemetryObject) { + this.incrementOutstandingRequests(); + + return this.openmct.telemetry.request(telemetryObject) + .then(telemetryData => { + let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let columnMap = this.getColumnMapForObject(keyString); + let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + + let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + this.boundedRows.add(telemetryRows); + console.log('Loaded %i rows', telemetryRows.length); + this.decrementOutstandingRequests(); + }); + } + + /** + * @private + */ + incrementOutstandingRequests() { + if (this.outstandingRequests === 0){ + this.emit('outstanding-requests', true); + } + this.outstandingRequests++; + } + + /** + * @private + */ + decrementOutstandingRequests() { + this.outstandingRequests--; + + if (this.outstandingRequests === 0){ + this.emit('outstanding-requests', false); + } + } + + refreshData(bounds, isTick) { + if (!isTick) { + this.filteredRows.clear(); + this.boundedRows.clear(); + this.telemetryObjects.forEach(this.requestDataFor); + } + } + + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); + + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; + return map; + }, {}); + } + + subscribeTo(telemetryObject) { + let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let columnMap = this.getColumnMapForObject(keyString); + let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + + this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => { + this.boundedRows.add(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + }); + } + + isTelemetryObject(domainObject) { + return domainObject.hasOwnProperty('telemetry'); + } + + unsubscribe(keyString) { + this.subscriptions[keyString](); + delete this.subscriptions[keyString]; + } + + destroy() { + this.boundedRows.destroy(); + this.filteredRows.destroy(); + Object.keys(this.subscriptions).forEach(this.unsubscribe, this); + this.openmct.time.off('bounds', this.refreshData); + + if (this.tableComposition !== undefined) { + this.tableComposition.off('add', this.addTelemetryObject); + this.tableComposition.off('remove', this.removeTelemetryObject); + } + } + } + + return TelemetryTable; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableColumn.js b/src/plugins/telemetryTable/TelemetryTableColumn.js new file mode 100644 index 0000000000..412aca7616 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableColumn.js @@ -0,0 +1,57 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +define(function () { + class TelemetryTableColumn { + constructor (openmct, metadatum) { + this.metadatum = metadatum; + this.formatter = openmct.telemetry.getValueFormatter(metadatum); + this.titleValue = this.metadatum.name; + } + + getKey() { + return this.metadatum.key; + } + + getTitle() { + return this.metadatum.name; + } + + getMetadatum() { + return this.metadatum; + } + + hasValueForDatum(telemetryDatum) { + return telemetryDatum.hasOwnProperty(this.metadatum.source); + } + + getRawValue(telemetryDatum) { + return telemetryDatum[this.metadatum.source]; + } + + getFormattedValue(telemetryDatum) { + return this.formatter.format(telemetryDatum); + } + + }; + + return TelemetryTableColumn; +}); diff --git a/src/plugins/telemetryTable/TelemetryTableComponent.js b/src/plugins/telemetryTable/TelemetryTableComponent.js new file mode 100644 index 0000000000..72ba2bfdea --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableComponent.js @@ -0,0 +1,315 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + define([ + 'lodash', + 'vue', + './telemetry-table.html', + './TelemetryTable', + './TelemetryTableRowComponent', + '../../exporters/CSVExporter' +],function ( + _, + Vue, + TelemetryTableTemplate, + TelemetryTable, + TelemetryTableRowComponent, + CSVExporter +) { + const VISIBLE_ROW_COUNT = 100; + const ROW_HEIGHT = 17; + const RESIZE_POLL_INTERVAL = 200; + const AUTO_SCROLL_TRIGGER_HEIGHT = 20; + + return function TelemetryTableComponent(domainObject, openmct) { + const csvExporter = new CSVExporter(); + const table = new TelemetryTable(domainObject, VISIBLE_ROW_COUNT, openmct); + let processingScroll = false; + let updatingView = false; + + return new Vue({ + template: TelemetryTableTemplate, + components: { + 'telemetry-table-row': TelemetryTableRowComponent + }, + data() { + return { + headers: {}, + configuration: table.configuration.getConfiguration(), + headersCount: 0, + visibleRows: [], + columnWidths: [], + sizingRows: {}, + rowHeight: ROW_HEIGHT, + scrollOffset: 0, + totalHeight: 0, + totalWidth: 0, + rowOffset: 0, + autoScroll: true, + sortOptions: {}, + filters: {}, + loading: false, + scrollable: undefined, + tableEl: undefined, + headersHolderEl: undefined, + calcTableWidth: '100%' + } + }, + methods: { + updateVisibleRows() { + + let start = 0; + let end = VISIBLE_ROW_COUNT; + let filteredRows = table.filteredRows.getRows(); + let filteredRowsLength = filteredRows.length; + + this.totalHeight = this.rowHeight * filteredRowsLength - 1; + + if (filteredRowsLength < VISIBLE_ROW_COUNT) { + end = filteredRowsLength; + } else { + let firstVisible = this.calculateFirstVisibleRow(); + let lastVisible = this.calculateLastVisibleRow(); + let totalVisible = lastVisible - firstVisible; + + let numberOffscreen = VISIBLE_ROW_COUNT - totalVisible; + start = firstVisible - Math.floor(numberOffscreen / 2); + end = lastVisible + Math.ceil(numberOffscreen / 2); + + if (start < 0) { + start = 0; + end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength); + } else if (end >= filteredRowsLength) { + end = filteredRowsLength; + start = end - VISIBLE_ROW_COUNT + 1; + } + } + this.rowOffset = start; + this.visibleRows = filteredRows.slice(start, end); + }, + calculateFirstVisibleRow() { + return Math.floor(this.scrollable.scrollTop / this.rowHeight); + }, + calculateLastVisibleRow() { + let bottomScroll = this.scrollable.scrollTop + this.scrollable.offsetHeight; + return Math.floor(bottomScroll / this.rowHeight); + }, + updateHeaders() { + let headers = table.configuration.getVisibleHeaders(); + + this.headers = headers; + this.headersCount = Object.values(headers).length; + Vue.nextTick().then(this.calculateColumnWidths); + }, + setSizingTableWidth() { + let scrollW = this.scrollable.offsetWidth - this.scrollable.clientWidth; + + if (scrollW && scrollW > 0) { + this.calcTableWidth = 'calc(100% - ' + scrollW + 'px)'; + } + }, + calculateColumnWidths() { + let columnWidths = []; + let totalWidth = 0; + let sizingRowEl = this.sizingTable.children[0]; + let sizingCells = Array.from(sizingRowEl.children); + + sizingCells.forEach((cell) => { + let columnWidth = cell.offsetWidth; + columnWidths.push(columnWidth + 'px'); + totalWidth += columnWidth; + }); + + this.columnWidths = columnWidths; + this.totalWidth = totalWidth; + }, + sortBy(columnKey) { + // If sorting by the same column, flip the sort direction. + if (this.sortOptions.key === columnKey) { + if (this.sortOptions.direction === 'asc') { + this.sortOptions.direction = 'desc'; + } else { + this.sortOptions.direction = 'asc'; + } + } else { + this.sortOptions = { + key: columnKey, + direction: 'asc' + } + } + table.filteredRows.sortBy(this.sortOptions); + }, + scroll() { + if (!processingScroll) { + processingScroll = true; + requestAnimationFrame(()=> { + this.updateVisibleRows(); + this.synchronizeScrollX(); + + if (this.shouldSnapToBottom()) { + this.autoScroll = true; + } else { + // If user scrolls away from bottom, disable auto-scroll. + // Auto-scroll will be re-enabled if user scrolls to bottom again. + this.autoScroll = false; + } + processingScroll = false; + }); + } + }, + shouldSnapToBottom() { + return this.scrollable.scrollTop >= (this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT); + }, + scrollToBottom() { + this.scrollable.scrollTop = this.scrollable.scrollHeight; + }, + synchronizeScrollX() { + this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft; + }, + filterChanged(columnKey) { + table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); + }, + clearFilter(columnKey) { + this.filters[columnKey] = ''; + table.filteredRows.setColumnFilter(columnKey, ''); + }, + rowsAdded(rows) { + let sizingRow; + if (Array.isArray(rows)) { + sizingRow = rows[0]; + } else { + sizingRow = rows; + } + if (!this.sizingRows[sizingRow.objectKeyString]) { + this.sizingRows[sizingRow.objectKeyString] = sizingRow; + Vue.nextTick().then(this.calculateColumnWidths); + } + + if (!updatingView) { + updatingView = true; + requestAnimationFrame(()=> { + this.updateVisibleRows(); + if (this.autoScroll) { + Vue.nextTick().then(this.scrollToBottom); + } + updatingView = false; + }); + } + }, + rowsRemoved(rows) { + if (!updatingView) { + updatingView = true; + requestAnimationFrame(()=> { + this.updateVisibleRows(); + updatingView = false; + }); + } + }, + exportAsCSV() { + const justTheData = table.filteredRows.getRows() + .map(row => row.getFormattedDatum()); + const headers = Object.keys(this.headers); + csvExporter.export(justTheData, { + filename: table.domainObject.name + '.csv', + headers: headers + }); + }, + outstandingRequests(loading) { + this.loading = loading; + }, + calculateTableSize() { + this.setSizingTableWidth(); + Vue.nextTick().then(this.calculateColumnWidths); + }, + pollForResize() { + let el = this.$el; + let width = el.clientWidth; + let height = el.clientHeight; + + this.resizePollHandle = setInterval(() => { + if (el.clientWidth !== width || el.clientHeight !== height) { + this.calculateTableSize(); + width = el.clientWidth; + height = el.clientHeight; + } + }, RESIZE_POLL_INTERVAL); + }, + updateConfiguration(configuration) { + this.configuration = configuration; + this.updateHeaders(); + }, + addObject() { + this.updateHeaders(); + }, + removeObject(objectIdentifier) { + let objectKeyString = openmct.objects.makeKeyString(objectIdentifier); + delete this.sizingRows[objectKeyString]; + this.updateHeaders(); + } + }, + created() { + this.filterChanged = _.debounce(this.filterChanged, 500); + }, + mounted() { + table.on('object-added', this.addObject); + table.on('object-removed', this.removeObject); + table.on('outstanding-requests', this.outstandingRequests); + + table.filteredRows.on('add', this.rowsAdded); + table.filteredRows.on('remove', this.rowsRemoved); + table.filteredRows.on('sort', this.updateVisibleRows); + table.filteredRows.on('filter', this.updateVisibleRows); + + //Default sort + this.sortOptions = table.filteredRows.sortBy(); + this.scrollable = this.$el.querySelector('.t-scrolling'); + this.sizingTable = this.$el.querySelector('.js-sizing-table'); + this.headersHolderEl = this.$el.querySelector('.mct-table-headers-w'); + + table.configuration.on('change', this.updateConfiguration); + + this.calculateTableSize(); + this.pollForResize(); + + table.initialize(); + }, + destroyed() { + table.off('object-added', this.addObject); + table.off('object-removed', this.removeObject); + table.off('outstanding-requests', this.outstandingRequests); + + table.filteredRows.off('add', this.rowsAdded); + table.filteredRows.off('remove', this.rowsRemoved); + table.filteredRows.off('sort', this.updateVisibleRows); + table.filteredRows.off('filter', this.updateVisibleRows); + + table.configuration.off('change', this.updateConfiguration); + + clearInterval(this.resizePollHandle); + + table.configuration.destroy(); + + table.destroy(); + } + }); + } + }); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableConfiguration.js b/src/plugins/telemetryTable/TelemetryTableConfiguration.js new file mode 100644 index 0000000000..e29a9715c9 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableConfiguration.js @@ -0,0 +1,141 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + 'lodash', + 'EventEmitter', + './TelemetryTableColumn', +], function (_, EventEmitter, TelemetryTableColumn) { + + class TelemetryTableConfiguration extends EventEmitter{ + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + this.columns = {}; + + this.addColumnsForObject = this.addColumnsForObject.bind(this); + this.removeColumnsForObject = this.removeColumnsForObject.bind(this); + this.objectMutated = this.objectMutated.bind(this); + + this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated); + } + + getConfiguration() { + let configuration = this.domainObject.configuration || {}; + configuration.hiddenColumns = configuration.hiddenColumns || {}; + return configuration; + } + + updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @private + * @param {*} object + */ + objectMutated(object) { + let oldConfiguration = this.domainObject.configuration; + + //Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible. + this.domainObject = JSON.parse(JSON.stringify(object)); + if (!_.eq(object.configuration, oldConfiguration)){ + this.emit('change', object.configuration); + } + } + + addColumnsForAllObjects(objects) { + objects.forEach(object => this.addColumnsForObject(object, false)); + } + + addColumnsForObject(telemetryObject) { + let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); + let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + this.columns[objectKeyString] = []; + + metadataValues.forEach(metadatum => { + let column = new TelemetryTableColumn(this.openmct, metadatum); + this.columns[objectKeyString].push(column); + }); + } + + removeColumnsForObject(objectIdentifier) { + let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier); + let columnsToRemove = this.columns[objectKeyString]; + + delete this.columns[objectKeyString]; + columnsToRemove.forEach((column) => { + //There may be more than one column with the same key (eg. time system columns) + if (!this.hasColumnWithKey(column.getKey())) { + let configuration = this.domainObject.configuration; + delete configuration.hiddenColumns[column.getKey()]; + // If there are no more columns with this key, delete any configuration, and trigger + // a column refresh. + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + }); + } + + hasColumnWithKey(columnKey) { + return _.flatten(Object.values(this.columns)) + .findIndex(column => column.getKey() === columnKey) !== -1; + } + + getColumns() { + return this.columns; + } + + getAllHeaders() { + let flattenedColumns = _.flatten(Object.values(this.columns)); + let headers = _.uniq(flattenedColumns, false, column => column.getKey()) + .reduce(fromColumnsToHeadersMap, {}); + + function fromColumnsToHeadersMap(headersMap, column){ + headersMap[column.getKey()] = column.getTitle(); + return headersMap; + } + + return headers; + } + + getVisibleHeaders() { + let headers = this.getAllHeaders(); + let configuration = this.getConfiguration(); + + Object.keys(headers).forEach((headerKey) => { + if (configuration.hiddenColumns[headerKey] === true) { + delete headers[headerKey]; + } + }); + + return headers; + } + + destroy() { + this.unlistenFromMutation(); + } + } + + return TelemetryTableConfiguration; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableRow.js b/src/plugins/telemetryTable/TelemetryTableRow.js new file mode 100644 index 0000000000..804a4ef44d --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableRow.js @@ -0,0 +1,82 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([], function () { + class TelemetryTableRow { + constructor(datum, columns, objectKeyString, limitEvaluator) { + this.columns = columns; + + this.datum = createNormalizedDatum(datum, columns); + this.limitEvaluator = limitEvaluator; + this.objectKeyString = objectKeyString; + } + + getFormattedDatum() { + return Object.values(this.columns) + .reduce((formattedDatum, column) => { + formattedDatum[column.getKey()] = this.getFormattedValue(column.getKey()); + return formattedDatum; + }, {}); + } + + getFormattedValue(key) { + let column = this.columns[key]; + return column.getFormattedValue(this.datum[key]); + } + + getRowLimitClass() { + if (!this.rowLimitClass) { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum); + this.rowLimitClass = limitEvaluation && limitEvaluation.cssClass; + } + return this.rowLimitClass; + } + + getCellLimitClasses() { + if (!this.cellLimitClasses) { + this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); + alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; + + return alarmStateMap; + }, {}); + } + return this.cellLimitClasses; + } + } + + /** + * Normalize the structure of datums to assist sorting and merging of columns. + * Maps all sources to keys. + * @private + * @param {*} telemetryDatum + * @param {*} metadataValues + */ + function createNormalizedDatum(datum, columns) { + return Object.values(columns).reduce((normalizedDatum, column) => { + normalizedDatum[column.getKey()] = column.getRawValue(datum); + return normalizedDatum; + }, {}); + } + + return TelemetryTableRow; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableRowComponent.js b/src/plugins/telemetryTable/TelemetryTableRowComponent.js new file mode 100644 index 0000000000..6fdac03979 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableRowComponent.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + define([ + './telemetry-table-row.html', +],function ( + TelemetryTableRowTemplate +) { + return { + template: TelemetryTableRowTemplate, + data: function () { + return { + rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px', + formattedRow: this.row.getFormattedDatum(), + rowLimitClass: this.row.getRowLimitClass(), + cellLimitClasses: this.row.getCellLimitClasses() + } + }, + props: { + headers: { + type: Object, + required: true + }, + row: { + type: Object, + required: true + }, + columnWidths: { + type: Array, + required: false, + default: [], + }, + rowIndex: { + type: Number, + required: false, + default: undefined + }, + rowOffset: { + type: Number, + required: false, + default: 0 + }, + rowHeight: { + type: Number, + required: false, + default: 0 + }, + configuration: { + type: Object, + required: true + } + }, + methods: { + calculateRowTop: function (rowOffset) { + this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px'; + }, + formatRow: function (row) { + this.formattedRow = row.getFormattedDatum(); + this.rowLimitClass = row.getRowLimitClass(); + this.cellLimitClasses = row.getCellLimitClasses(); + } + }, + watch: { + rowOffset: 'calculateRowTop', + row: { + handler: 'formatRow', + deep: false + } + } + }; + }); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableType.js b/src/plugins/telemetryTable/TelemetryTableType.js new file mode 100644 index 0000000000..395a81fe68 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableType.js @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define(function () { + function TelemetryTableType() { + return { + name: 'Telemetry Table', + description: 'Display telemetry values for the current time bounds in tabular form. Supports filtering and sorting.', + creatable: true, + cssClass: 'icon-tabular-realtime', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + columns: {} + }; + } + } + } + return TelemetryTableType; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/TelemetryTableViewProvider.js b/src/plugins/telemetryTable/TelemetryTableViewProvider.js new file mode 100644 index 0000000000..c162ce4d90 --- /dev/null +++ b/src/plugins/telemetryTable/TelemetryTableViewProvider.js @@ -0,0 +1,52 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define(['./TelemetryTableComponent'], function (TelemetryTableComponent) { + function TelemetryTableViewProvider(openmct) { + return { + key: 'table', + name: 'Telemetry Table', + editable: true, + canView: function (domainObject) { + return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry'); + }, + view: function (domainObject) { + let component; + return { + show: function (element) { + component = new TelemetryTableComponent(domainObject, openmct); + element.appendChild(component.$mount().$el); + }, + destroy: function (element) { + component.$destroy(); + element.removeChild(component.$el); + component = undefined; + } + } + }, + priority: function () { + return 1; + } + } + } + return TelemetryTableViewProvider; +}); \ No newline at end of file diff --git a/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js b/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js new file mode 100644 index 0000000000..1157a3ef49 --- /dev/null +++ b/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js @@ -0,0 +1,139 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [ + 'lodash', + './SortedTableRowCollection' + ], + function ( + _, + SortedTableRowCollection + ) { + + class BoundedTableRowCollection extends SortedTableRowCollection { + constructor (openmct) { + super(); + + this.futureBuffer = new SortedTableRowCollection(); + this.openmct = openmct; + + this.sortByTimeSystem = this.sortByTimeSystem.bind(this) + this.bounds = this.bounds.bind(this) + + this.sortByTimeSystem(openmct.time.timeSystem()); + openmct.time.on('timeSystem', this.sortByTimeSystem); + + this.lastBounds = openmct.time.bounds(); + openmct.time.on('bounds', this.bounds); + } + + addOne (item) { + // Insert into either in-bounds array, or the future buffer. + // Data in the future buffer will be re-evaluated for possible + // insertion on next bounds change + let beforeStartOfBounds = item.datum[this.sortOptions.key] < this.lastBounds.start; + let afterEndOfBounds = item.datum[this.sortOptions.key] > this.lastBounds.end; + + if (!afterEndOfBounds && !beforeStartOfBounds) { + return super.addOne(item); + } else if (afterEndOfBounds) { + this.futureBuffer.addOne(item); + } + return false; + } + + sortByTimeSystem(timeSystem) { + this.sortBy({key: timeSystem.key, direction: 'asc'}); + this.futureBuffer.sortBy({key: timeSystem.key, direction: 'asc'}); + } + + /** + * This function is optimized for ticking - it assumes that start and end + * bounds will only increase and as such this cannot be used for decreasing + * bounds changes. + * + * An implication of this is that data will not be discarded that exceeds + * the given end bounds. For arbitrary bounds changes, it's assumed that + * a telemetry requery is performed anyway, and the collection is cleared + * and repopulated. + * + * @fires TelemetryCollection#added + * @fires TelemetryCollection#discarded + * @param bounds + */ + bounds (bounds) { + let startChanged = this.lastBounds.start !== bounds.start; + let endChanged = this.lastBounds.end !== bounds.end; + + let startIndex = 0; + let endIndex = 0; + + let discarded = []; + let added = []; + let testValue = { + datum: {} + }; + + this.lastBounds = bounds; + + if (startChanged) { + testValue.datum[this.sortOptions.key] = bounds.start; + // Calculate the new index of the first item within the bounds + startIndex = this.sortedIndex(this.rows, testValue); + discarded = this.rows.splice(0, startIndex); + } + + if (endChanged) { + testValue.datum[this.sortOptions.key] = bounds.end; + // Calculate the new index of the last item in bounds + endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue); + added = this.futureBuffer.rows.splice(0, endIndex); + added.forEach((datum) => this.rows.push(datum)); + } + + if (discarded && discarded.length > 0) { + /** + * A `discarded` event is emitted when telemetry data fall out of + * bounds due to a bounds change event + * @type {object[]} discarded the telemetry data + * discarded as a result of the bounds change + */ + this.emit('remove', discarded); + } + if (added && added.length > 0) { + /** + * An `added` event is emitted when a bounds change results in + * received telemetry falling within the new bounds. + * @type {object[]} added the telemetry data that is now within bounds + */ + this.emit('add', added); + } + } + + destroy() { + this.openmct.time.off('timeSystem', this.sortByTimeSystem); + this.openmct.time.off('bounds', this.bounds); + } + } + return BoundedTableRowCollection; +}); diff --git a/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js b/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js new file mode 100644 index 0000000000..14f866fb49 --- /dev/null +++ b/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js @@ -0,0 +1,112 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [ + './SortedTableRowCollection' + ], + function ( + SortedTableRowCollection + ) { + class FilteredTableRowCollection extends SortedTableRowCollection { + constructor(masterCollection) { + super(); + + this.masterCollection = masterCollection; + this.columnFilters = {}; + + //Synchronize with master collection + this.masterCollection.on('add', this.add); + this.masterCollection.on('remove', this.remove); + + //Default to master collection's sort options + this.sortOptions = masterCollection.sortBy(); + } + + setColumnFilter(columnKey, filter) { + filter = filter.trim().toLowerCase(); + + let rowsToFilter = this.getRowsToFilter(columnKey, filter); + if (filter.length === 0) { + delete this.columnFilters[columnKey]; + } else { + this.columnFilters[columnKey] = filter; + } + this.rows = rowsToFilter.filter(this.matchesFilters, this); + this.emit('filter'); + } + + /** + * @private + */ + getRowsToFilter(columnKey, filter) { + if (this.isSubsetOfCurrentFilter(columnKey, filter)) { + return this.getRows(); + } else { + return this.masterCollection.getRows(); + } + } + + /** + * @private + */ + isSubsetOfCurrentFilter(columnKey, filter) { + return this.columnFilters[columnKey] && + filter.startsWith(this.columnFilters[columnKey]) && + // startsWith check will otherwise fail when filter cleared + // because anyString.startsWith('') === true + filter !== ''; + } + + addOne(row) { + return this.matchesFilters(row) && super.addOne(row); + } + + /** + * @private + */ + matchesFilters(row) { + let doesMatchFilters = true; + for (const key in this.columnFilters) { + if (!this.rowHasColumn(row, key)) { + return false; + } else { + let formattedValue = row.getFormattedValue(key).toLowerCase(); + doesMatchFilters = doesMatchFilters && + formattedValue.indexOf(this.columnFilters[key]) !== -1; + } + } + return doesMatchFilters; + } + + rowHasColumn(row, key) { + return row.columns.hasOwnProperty(key); + } + + destroy() { + this.masterCollection.off('add', this.add); + this.masterCollection.off('remove', this.remove); + } + } + + return FilteredTableRowCollection; + }); \ No newline at end of file diff --git a/src/plugins/telemetryTable/collections/SortedTableRowCollection.js b/src/plugins/telemetryTable/collections/SortedTableRowCollection.js new file mode 100644 index 0000000000..756896f25f --- /dev/null +++ b/src/plugins/telemetryTable/collections/SortedTableRowCollection.js @@ -0,0 +1,218 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [ + 'lodash', + 'EventEmitter' + ], + function ( + _, + EventEmitter + ) { + const LESS_THAN = -1; + const EQUAL = 0; + const GREATER_THAN = 1; + + /** + * @constructor + */ + class SortedTableRowCollection extends EventEmitter { + constructor () { + super(); + + this.dupeCheck = false; + this.rows = []; + + this.add = this.add.bind(this); + this.remove = this.remove.bind(this); + } + + /** + * Add a datum or array of data to this telemetry collection + * @fires TelemetryCollection#added + * @param {object | object[]} rows + */ + add(rows) { + if (Array.isArray(rows)) { + this.dupeCheck = false; + + let rowsAdded = rows.filter(this.addOne, this); + if (rowsAdded.length > 0) { + this.emit('add', rowsAdded); + } + this.dupeCheck = true; + } else { + let wasAdded = this.addOne(rows); + if (wasAdded) { + this.emit('add', rows); + } + } + } + + /** + * @private + */ + addOne(row) { + if (this.sortOptions === undefined) { + throw 'Please specify sort options'; + } + + let isDuplicate = false; + + // Going to check for duplicates. Bound the search problem to + // items around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. + let startIx = this.sortedIndex(this.rows, row); + let endIx = undefined; + + if (this.dupeCheck && startIx !== this.rows.length) { + endIx = this.sortedLastIndex(this.rows, row); + + // Create an array of potential dupes, based on having the + // same time stamp + let potentialDupes = this.rows.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, row)) > -1; + } + + if (!isDuplicate) { + this.rows.splice(endIx || startIx, 0, row); + return true; + } + return false; + } + + sortedLastIndex(rows, testRow) { + return this.sortedIndex(rows, testRow, _.sortedLastIndex); + } + /** + * Finds the correct insertion point for the given row. + * Leverages lodash's `sortedIndex` function which implements a binary search. + * @private + */ + sortedIndex(rows, testRow, lodashFunction) { + const sortOptionsKey = this.sortOptions.key; + lodashFunction = lodashFunction || _.sortedIndex; + + if (this.sortOptions.direction === 'asc') { + return lodashFunction(rows, testRow, (thisRow) => { + return thisRow.datum[sortOptionsKey]; + }); + } else { + const testRowValue = testRow.datum[this.sortOptions.key]; + // Use a custom comparison function to support descending sort. + return lodashFunction(rows, testRow, (thisRow) => { + const thisRowValue = thisRow.datum[sortOptionsKey]; + if (testRowValue === thisRowValue) { + return EQUAL; + } else if (testRowValue < thisRowValue) { + return LESS_THAN; + } else { + return GREATER_THAN; + } + }); + } + } + + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. Subsequent inserts are sorted to maintain specified sport + * order. + * + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sortBy({ + * key: 'value', direction: 'asc' + * }); + * + * // Sort by ms since epoch + * collection.sort({ + * key: 'timestamp.ms', + * direction: 'asc' + * }); + * + * // Sort by 'text' attribute, descending + * collection.sort("timestamp.text"); + * + * + * @param {object} sortOptions An object specifying a sort key, and direction. + */ + sortBy(sortOptions) { + if (arguments.length > 0) { + this.sortOptions = sortOptions; + this.rows = _.sortByOrder(this.rows, 'datum.' + sortOptions.key, sortOptions.direction); + this.emit('sort'); + } + // Return duplicate to avoid direct modification of underlying object + return Object.assign({}, this.sortOptions); + } + + removeAllRowsForObject(objectKeyString) { + let removed = []; + this.rows = this.rows.filter(row => { + if (row.objectKeyString === objectKeyString) { + removed.push(row); + return false; + } + return true; + }); + this.emit('remove', removed); + } + + remove(removedRows) { + this.rows = this.rows.filter(row => { + return removedRows.indexOf(row) === -1; + }); + this.emit('remove', removedRows); + } + + getRows () { + return this.rows; + } + + clear() { + let removedRows = this.rows; + this.rows = []; + this.emit('remove', removedRows); + } + } + return SortedTableRowCollection; +}); diff --git a/src/plugins/telemetryTable/plugin.js b/src/plugins/telemetryTable/plugin.js new file mode 100644 index 0000000000..1cc0750ef0 --- /dev/null +++ b/src/plugins/telemetryTable/plugin.js @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + define([ + './TelemetryTableViewProvider', + './TableConfigurationViewProvider', + './TelemetryTableType' + ], function ( + TelemetryTableViewProvider, + TableConfigurationViewProvider, + TelemetryTableType + ) { + return function plugin() { + return function install(openmct) { + openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); + openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); + openmct.types.addType('table', TelemetryTableType()); + }; + }; + }); \ No newline at end of file diff --git a/src/plugins/telemetryTable/table-configuration.html b/src/plugins/telemetryTable/table-configuration.html new file mode 100644 index 0000000000..b21e1e7fe1 --- /dev/null +++ b/src/plugins/telemetryTable/table-configuration.html @@ -0,0 +1,11 @@ +
+ + + +
\ No newline at end of file diff --git a/src/plugins/telemetryTable/telemetry-table-row.html b/src/plugins/telemetryTable/telemetry-table-row.html new file mode 100644 index 0000000000..e77872e640 --- /dev/null +++ b/src/plugins/telemetryTable/telemetry-table-row.html @@ -0,0 +1,6 @@ + + {{formattedRow[key]}} + \ No newline at end of file diff --git a/src/plugins/telemetryTable/telemetry-table.html b/src/plugins/telemetryTable/telemetry-table.html new file mode 100644 index 0000000000..78fa917a2c --- /dev/null +++ b/src/plugins/telemetryTable/telemetry-table.html @@ -0,0 +1,64 @@ +
+ + +
+ + + + + + + + + +
{{title}}
+
+ + +
+
+
+ +
+
+ + + + + +
+
+ + + + + + + +
{{title}}
+
\ No newline at end of file diff --git a/src/styles/_table.scss b/src/styles/_table.scss index 534da1c432..7b2deccdc0 100644 --- a/src/styles/_table.scss +++ b/src/styles/_table.scss @@ -20,47 +20,52 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -mct-table { - .mct-sizing-table { - z-index: -1; - visibility: hidden; - position: absolute !important; +.mct-sizing-table { + z-index: -1; + visibility: hidden; + position: absolute !important; + + //Add some padding to allow for decorations such as limits indicator + td { + padding-right: 15px; + padding-left: 10px; + white-space: nowrap; + } +} + +.mct-table { + tr { + display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define + align-items: stretch; + } + + td, th { + box-sizing: border-box; + display: block; + flex: 1 0 auto; + white-space: nowrap; + } + + thead { + display: block; + } + + tbody { + tr { + position: absolute; + height: 18px; // Needed when a row has empty values in its cells + } - //Add some padding to allow for decorations such as limits indicator td { - padding-right: 15px; - padding-left: 10px; - white-space: nowrap; - } - } - - .mct-table { - thead { - display: block; - tr { - display: block; - white-space: nowrap; - th { - display: inline-block; - box-sizing: border-box; - } - } - } - tbody { - tr { - position: absolute; - white-space: nowrap; - display: block; - } - td { - white-space: nowrap; - overflow: hidden; - box-sizing: border-box; - display: inline-block; - } + overflow: hidden; + box-sizing: border-box; + display: inline-block; + text-overflow: ellipsis; } } +} +.l-telemetry-table { .l-control-bar { margin-bottom: 3px; }