Merge branch 'master' into open671

This commit is contained in:
Pete Richards
2016-05-09 10:19:24 -07:00
70 changed files with 2241 additions and 1786 deletions

View File

@ -45,7 +45,7 @@ define(
* @param metadata Metadata describing the domains and ranges available
* @returns {TableConfiguration} This object
*/
TableConfiguration.prototype.buildColumns = function (metadata) {
TableConfiguration.prototype.populateColumns = function (metadata) {
var self = this;
this.columns = [];
@ -138,8 +138,7 @@ define(
* @private
*/
TableConfiguration.prototype.defaultColumnConfiguration = function () {
return ((this.domainObject.getModel().configuration || {}).table ||
{}).columns || {};
return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
};
/**
@ -154,6 +153,16 @@ define(
});
};
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.
@ -161,7 +170,7 @@ define(
* pairs where the key is the column title, and the value is a
* boolean indicating whether the column should be shown.
*/
TableConfiguration.prototype.getColumnConfiguration = function () {
TableConfiguration.prototype.buildColumnConfiguration = function () {
var configuration = {},
//Use existing persisted config, or default it
defaultConfig = this.defaultColumnConfiguration();
@ -177,6 +186,11 @@ define(
defaultConfig[columnTitle];
});
//Synchronize column configuration with model
if (configChanged(configuration, defaultConfig)) {
this.saveColumnConfiguration(configuration);
}
return configuration;
};

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./TelemetryTableController'
],
function (TableController) {
/**
* Extends TelemetryTableController and adds real-time streaming
* support.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor
*/
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
}
HistoricalTableController.prototype = Object.create(TableController.prototype);
/**
* Populates historical data on scope when it becomes available from
* the telemetry API
*/
HistoricalTableController.prototype.addHistoricalData = function () {
var rowData = [],
self = this;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
var series = self.handle.getSeries(telemetryObject) || {},
pointCount = series.getPointCount ? series.getPointCount() : 0,
i = 0;
for (; i < pointCount; i++) {
rowData.push(self.table.getRowValues(telemetryObject,
self.handle.makeDatum(telemetryObject, series, i)));
}
});
this.$scope.rows = rowData;
};
return HistoricalTableController;
}
);

View File

@ -21,10 +21,13 @@ define(
this.maxDisplayRows = 50;
this.scrollable = element.find('div');
this.thead = element.find('thead');
this.tbody = element.find('tbody');
this.$scope.sizingRow = {};
this.scrollable.on('scroll', this.onScroll.bind(this));
$scope.visibleRows = [];
$scope.overrideRowPositioning = false;
/**
* Set default values for optional parameters on a given scope
@ -56,22 +59,22 @@ define(
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
self.updateRows($scope.rows);
self.setRows($scope.rows);
};
/*
* Define watches to listen for changes to headers and rows.
*/
$scope.$watchCollection('filters', function () {
self.updateRows($scope.rows);
self.setRows($scope.rows);
});
$scope.$watch('headers', this.updateHeaders.bind(this));
$scope.$watch('rows', this.updateRows.bind(this));
$scope.$watch('headers', this.setHeaders.bind(this));
$scope.$watch('rows', this.setRows.bind(this));
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('add:row', this.newRow.bind(this));
$scope.$on('add:row', this.addRow.bind(this));
$scope.$on('remove:row', this.removeRow.bind(this));
}
@ -94,23 +97,27 @@ define(
/**
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* `add:row` broadcast event.
* @private
*/
MCTTableController.prototype.newRow = function (event, rowIndex) {
MCTTableController.prototype.addRow = function (event, rowIndex) {
var row = this.$scope.rows[rowIndex];
//Add row to the filtered, sorted list of all rows
if (this.filterRows([row]).length > 0) {
this.insertSorted(this.$scope.displayRows, row);
}
this.$timeout(this.setElementSizes.bind(this))
.then(this.scrollToBottom.bind(this));
//Does the row pass the current filter?
if (this.filterRows([row]).length === 1) {
//Insert the row into the correct position in the array
this.insertSorted(this.$scope.displayRows, row);
//Resize the columns , then update the rows visible in the table
this.resize([this.$scope.sizingRow, row])
.then(this.setVisibleRows.bind(this))
.then(this.scrollToBottom.bind(this));
}
};
/**
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* Handles a row remove event. Rows can be removed as needed using the
* `remove:row` broadcast event.
* @private
*/
MCTTableController.prototype.removeRow = function (event, rowIndex) {
@ -223,7 +230,7 @@ define(
* enabled, reset filters. If sorting is enabled, reset
* sorting.
*/
MCTTableController.prototype.updateHeaders = function (newHeaders) {
MCTTableController.prototype.setHeaders = function (newHeaders) {
if (!newHeaders){
return;
}
@ -239,7 +246,7 @@ define(
this.$scope.sortColumn = undefined;
this.$scope.sortDirection = undefined;
}
this.updateRows(this.$scope.rows);
this.setRows(this.$scope.rows);
};
/**
@ -247,12 +254,12 @@ define(
* for individual rows.
*/
MCTTableController.prototype.setElementSizes = function () {
var thead = this.element.find('thead'),
tbody = this.element.find('tbody'),
var thead = this.thead,
tbody = this.tbody,
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
rowHeight = 20,
rowHeight = firstRow.prop('offsetHeight'),
columnWidth,
tableWidth = 0,
overallHeight = headerHeight + (rowHeight *
@ -269,15 +276,12 @@ define(
this.$scope.headerHeight = headerHeight;
this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight;
this.setVisibleRows();
if (tableWidth > 0) {
this.$scope.totalWidth = tableWidth + 'px';
} else {
this.$scope.totalWidth = 'none';
}
this.$scope.overrideRowPositioning = true;
};
/**
@ -289,21 +293,14 @@ define(
sortKey = this.$scope.sortColumn;
function binarySearch(searchArray, searchElement, min, max){
var sampleAt = Math.floor((max - min) / 2) + min,
valA,
valB;
var sampleAt = Math.floor((max - min) / 2) + min;
if (max < min) {
return min; // Element is not in array, min gives direction
}
valA = isNaN(searchElement[sortKey].text) ?
searchElement[sortKey].text :
parseFloat(searchElement[sortKey].text);
valB = isNaN(searchArray[sampleAt][sortKey].text) ?
searchArray[sampleAt][sortKey].text :
parseFloat(searchArray[sampleAt][sortKey].text);
switch(self.sortComparator(valA, valB)) {
switch(self.sortComparator(searchElement[sortKey].text,
searchArray[sampleAt][sortKey].text)) {
case -1:
return binarySearch(searchArray, searchElement, min,
sampleAt - 1);
@ -341,8 +338,34 @@ define(
*/
MCTTableController.prototype.sortComparator = function (a, b) {
var result = 0,
sortDirectionMultiplier;
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();
@ -379,15 +402,7 @@ define(
}
return rowsToSort.sort(function (a, b) {
//If the values to compare can be compared as
// numbers, do so. String comparison of number
// values can cause inconsistencies
var valA = isNaN(a[sortKey].text) ? a[sortKey].text :
parseFloat(a[sortKey].text),
valB = isNaN(b[sortKey].text) ? b[sortKey].text :
parseFloat(b[sortKey].text);
return self.sortComparator(valA, valB);
return self.sortComparator(a[sortKey].text, b[sortKey].text);
});
};
@ -397,74 +412,48 @@ define(
* pre-calculate optimal column sizes without having to render
* every row.
*/
MCTTableController.prototype.findLargestRow = function (rows) {
var largestRow = rows.reduce(function (largestRow, row) {
MCTTableController.prototype.buildLargestRow = function (rows) {
var largestRow = rows.reduce(function (prevLargest, row) {
Object.keys(row).forEach(function (key) {
var currentColumn = row[key].text,
var currentColumn,
currentColumnLength,
largestColumn,
largestColumnLength;
if (row[key]){
currentColumn = (row[key]).text;
currentColumnLength =
(currentColumn && currentColumn.length) ?
currentColumn.length :
currentColumn,
largestColumn = largestRow[key].text,
largestColumnLength =
(largestColumn && largestColumn.length) ?
largestColumn.length :
largestColumn;
currentColumn;
largestColumn = prevLargest[key] ? prevLargest[key].text : "";
largestColumnLength = largestColumn.length;
if (currentColumnLength > largestColumnLength) {
largestRow[key] = JSON.parse(JSON.stringify(row[key]));
if (currentColumnLength > largestColumnLength) {
prevLargest[key] = JSON.parse(JSON.stringify(row[key]));
}
}
});
return largestRow;
return prevLargest;
}, JSON.parse(JSON.stringify(rows[0] || {})));
largestRow = JSON.parse(JSON.stringify(largestRow));
// Pad with characters to accomodate variable-width fonts,
// and remove characters that would allow word-wrapping.
Object.keys(largestRow).forEach(function (key) {
var padCharacters,
i;
largestRow[key].text = String(largestRow[key].text);
padCharacters = largestRow[key].text.length / 10;
for (i = 0; i < padCharacters; i++) {
largestRow[key].text = largestRow[key].text + 'W';
}
largestRow[key].text = largestRow[key].text
.replace(/[ \-_]/g, 'W');
});
return largestRow;
};
/**
* Calculates the widest row in the table, pads that row, and adds
* it to the table. Allows the table to size itself, then uses this
* as basis for column dimensions.
* 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 (){
var largestRow = this.findLargestRow(this.$scope.displayRows),
self = this;
this.$scope.visibleRows = [
{
rowIndex: 0,
offsetY: undefined,
contents: largestRow
}
];
//Wait a timeout to allow digest of previous change to visible
// rows to happen.
this.$timeout(function () {
//Remove temporary padding row used for setting column widths
self.$scope.visibleRows = [];
self.setElementSizes();
});
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
return this.$timeout(this.setElementSizes.bind(this));
};
/**
* @priate
* @private
*/
MCTTableController.prototype.filterAndSort = function (rows) {
var displayRows = rows;
@ -475,26 +464,21 @@ define(
if (this.$scope.enableSort) {
displayRows = this.sortRows(displayRows.slice(0));
}
this.$scope.displayRows = displayRows;
return displayRows;
};
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.updateRows = function (newRows) {
//Reset visible rows because new row data available.
this.$scope.visibleRows = [];
this.$scope.overrideRowPositioning = false;
MCTTableController.prototype.setRows = function (newRows) {
//Nothing to show because no columns visible
if (!this.$scope.displayHeaders) {
if (!this.$scope.displayHeaders || !newRows) {
return;
}
this.filterAndSort(newRows || []);
this.resize();
this.$scope.displayRows = this.filterAndSort(newRows || []);
this.resize(newRows).then(this.setVisibleRows.bind(this));
};
/**

View File

@ -35,7 +35,7 @@ define(
* @param telemetryFormatter
* @constructor
*/
function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
$scope.autoScroll = false;
@ -64,58 +64,35 @@ define(
});
}
RTTelemetryTableController.prototype = Object.create(TableController.prototype);
RealtimeTableController.prototype = Object.create(TableController.prototype);
/**
Override the subscribe function defined on the parent controller in
order to handle realtime telemetry instead of historical.
* Overrides method on TelemetryTableController providing handling
* for realtime data.
*/
RTTelemetryTableController.prototype.subscribe = function () {
var self = this;
self.$scope.rows = undefined;
(this.subscriptions || []).forEach(function (unsubscribe){
unsubscribe();
});
RealtimeTableController.prototype.addRealtimeData = function() {
var self = this,
datum,
row;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
datum = self.handle.getDatum(telemetryObject);
if (datum) {
//Populate row values from telemetry datum
row = self.table.getRowValues(telemetryObject, datum);
self.$scope.rows.push(row);
if (this.handle) {
this.handle.unsubscribe();
}
function updateData(){
var datum,
row;
self.handle.getTelemetryObjects().forEach(function (telemetryObject){
datum = self.handle.getDatum(telemetryObject);
if (datum) {
row = self.table.getRowValues(telemetryObject, datum);
if (!self.$scope.rows){
self.$scope.rows = [row];
self.$scope.$digest();
} else {
self.$scope.rows.push(row);
if (self.$scope.rows.length > self.maxRows) {
self.$scope.$broadcast('remove:row', 0);
self.$scope.rows.shift();
}
self.$scope.$broadcast('add:row',
self.$scope.rows.length - 1);
}
//Inform table that a new row has been added
if (self.$scope.rows.length > self.maxRows) {
self.$scope.$broadcast('remove:row', 0);
self.$scope.rows.shift();
}
});
}
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,
updateData,
true // Lossless
);
this.setup();
self.$scope.$broadcast('add:row',
self.$scope.rows.length - 1);
}
});
};
return RTTelemetryTableController;
return RealtimeTableController;
}
);

View File

@ -49,13 +49,29 @@ define(
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.listeners = [];
$scope.columnsForm = {};
this.domainObject.getCapability('mutation').listen(function (model) {
self.populateForm(model);
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 (columns){
if (columns){
self.domainObject.useCapability('mutation', function (model) {
@ -65,6 +81,11 @@ define(
}
});
/**
* Destroy all mutation listeners
*/
$scope.$on('$destroy', unlisten);
}
TableOptionsController.prototype.populateForm = function (model) {
@ -84,7 +105,7 @@ define(
'key': key
});
});
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration));
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {}));
};
return TableOptionsController;

View File

@ -50,20 +50,15 @@ define(
this.$scope = $scope;
this.columns = {}; //Range and Domain columns
this.handle = undefined;
//this.pending = false;
this.telemetryHandler = telemetryHandler;
this.table = new TableConfiguration($scope.domainObject,
telemetryFormatter);
this.changeListeners = [];
$scope.rows = undefined;
$scope.rows = [];
// Subscribe to telemetry when a domain object becomes available
this.$scope.$watch('domainObject', function(domainObject){
if (!domainObject) {
return;
}
this.$scope.$watch('domainObject', function(){
self.subscribe();
self.registerChangeListeners();
});
@ -72,16 +67,24 @@ define(
this.$scope.$on("$destroy", this.destroy.bind(this));
}
/**
* @private
*/
TelemetryTableController.prototype.unregisterChangeListeners = function () {
this.changeListeners.forEach(function (listener) {
return listener && listener();
});
this.changeListeners = [];
};
/**
* Defer registration of change listeners until domain object is
* available in order to avoid race conditions
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
this.changeListeners.forEach(function (listener) {
return listener && listener();
});
this.changeListeners = [];
this.unregisterChangeListeners();
// When composition changes, re-subscribe to the various
// telemetry subscriptions
this.changeListeners.push(this.$scope.$watchCollection(
@ -102,6 +105,24 @@ define(
}
};
/**
* Function for handling realtime data when it is available. This
* will be called by the telemetry framework when new data is
* available.
*
* Method should be overridden by specializing class.
*/
TelemetryTableController.prototype.addRealtimeData = function () {
};
/**
* Function for handling historical data. Will be called by
* telemetry framework when requested historical data is available.
* Should be overridden by specializing class.
*/
TelemetryTableController.prototype.addHistoricalData = function () {
};
/**
Create a new subscription. This can be overridden by children to
change default behaviour (which is to retrieve historical telemetry
@ -112,13 +133,9 @@ define(
this.handle.unsubscribe();
}
//Noop because not supporting realtime data right now
function noop(){
}
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,
noop,
this.addRealtimeData.bind(this),
true // Lossless
);
@ -127,28 +144,6 @@ define(
this.setup();
};
/**
* Populates historical data on scope when it becomes available
* @private
*/
TelemetryTableController.prototype.addHistoricalData = function () {
var rowData = [],
self = this;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
var series = self.handle.getSeries(telemetryObject) || {},
pointCount = series.getPointCount ? series.getPointCount() : 0,
i = 0;
for (; i < pointCount; i++) {
rowData.push(self.table.getRowValues(telemetryObject,
self.handle.makeDatum(telemetryObject, series, i)));
}
});
this.$scope.rows = rowData;
};
/**
* Setup table columns based on domain object metadata
*/
@ -159,7 +154,9 @@ define(
if (handle) {
handle.promiseTelemetryObjects().then(function () {
table.buildColumns(handle.getMetadata());
self.$scope.headers = [];
self.$scope.rows = [];
table.populateColumns(handle.getMetadata());
self.filterColumns();
@ -173,26 +170,14 @@ define(
}
};
/**
* @private
* @param object The object for which data is available (table may
* be composed of multiple objects)
* @param datum The data received from the telemetry source
*/
TelemetryTableController.prototype.updateRows = function (object, datum) {
this.$scope.rows.push(this.table.getRowValues(object, datum));
};
/**
* When column configuration changes, update the visible headers
* accordingly.
* @private
*/
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
if (!columnConfig){
columnConfig = this.table.getColumnConfiguration();
this.table.saveColumnConfiguration(columnConfig);
}
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];

View File

@ -30,6 +30,51 @@ define(
* Defines a generic 'Table' component. The table can be populated
* en-masse by setting the rows attribute, or rows can be added as
* needed via a broadcast 'addRow' event.
*
* This directive accepts parameters specifying header and row
* content, as well as some additional options.
*
* Two broadcast events for notifying the table that the rows have
* changed. For performance reasons, the table does not monitor the
* content of `rows` constantly.
* - 'add:row': A $broadcast event that will notify the table that
* a new row has been added to the table.
* eg.
* <pre><code>
* $scope.rows.push(newRow);
* $scope.$broadcast('add:row', $scope.rows.length-1);
* </code></pre>
* 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.
* <pre><code>
* $scope.rows.slice(5, 1);
* $scope.$broadcast('remove:row', 5);
* </code></pre>
* The code above removes a row from the rows array, and then alerts
* the table to its removal.
*
* @memberof platform/features/table
* @param {string[]} headers The column titles to appear at the top
* of the table. Corresponding values are specified in the rows
* using the header title provided here.
* @param {Object[]} rows The row content. Each row is an object
* with key-value pairs where the key corresponds to a header
* specified in the headers parameter.
* @param {boolean} enableFilter If true, values will be searchable
* and results filtered
* @param {boolean} enableSort If true, sorting will be enabled
* allowing sorting by clicking on column headers
* @param {boolean} autoScroll If true, table will automatically
* scroll to the bottom as new data arrives. Auto-scroll can be
* disengaged manually by scrolling away from the bottom of the
* table, and can also be enabled manually by scrolling to the bottom of
* the table rows.
*
* @constructor
*/
function MCTTable($timeout) {