Merge pull request #839 from nasa/revert-822-open769

Revert "[Tables] Fix to correct sorting in realtime tables"
This commit is contained in:
Victor Woeltjen 2016-04-21 09:21:12 -07:00
commit 3a9b1ee901
18 changed files with 293 additions and 579 deletions

View File

@ -7,12 +7,11 @@ September 23, 2015
Document Version 1.1
Date | Version | Summary of Changes | Author
------------------- | --------- | ------------------------- | ---------------
------------------- | --------- | ----------------------- | ---------------
April 29, 2015 | 0 | Initial Draft | Victor Woeltjen
May 12, 2015 | 0.1 | | Victor Woeltjen
June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen
October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry
April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry
# Introduction
The purpose of this guide is to familiarize software developers with the Open
@ -1601,61 +1600,6 @@ there are items .
]
}
## Table
The `mct-table` directive provides a generic table component, with optional
sorting and filtering capabilities. The table can be pre-populated with data
by setting the `rows` parameter, and it can be updated in real-time using the
`add:row` and `remove:row` broadcast events. The table will expand to occupy
100% of the size of its containing element. The table is highly optimized for
very large data sets.
### Events
The table supports two events for notifying that the rows have changed. For
performance reasons, the table does not monitor the content of `rows`
constantly.
* `add:row`: A `$broadcast` event that will notify the table that a new row
has been added to the table.
eg. The code below adds a new row, and alerts the table using the `add:row`
event. Sorting and filtering will be applied automatically by the table component.
```
$scope.rows.push(newRow);
$scope.$broadcast('add:row', $scope.rows.length-1);
```
* `remove:row`: A `$broadcast` event that will notify the table that a row
should be removed from the table.
eg. The code below removes a row from the rows array, and then alerts the table
to its removal.
```
$scope.rows.slice(5, 1);
$scope.$broadcast('remove:row', 5);
```
### Parameters
* `headers`: An array of string values which will constitute the column titles
that appear at the top of the table. Corresponding values are specified in
the rows using the header title provided here.
* `rows`: An array of objects containing row values. Each element in the
array must be an associative array, where the key corresponds to a column header.
* `enableFilter`: A boolean that if true, will enable searching and result
filtering. When enabled, each column will have a text input field that can be
used to filter the table rows in real time.
* `enableSort`: A boolean determining whether rows can be sorted. If true,
sorting will be enabled allowing sorting by clicking on column headers. Only
one column may be sorted at a time.
* `autoScroll`: A boolean value that if true, will cause the table to automatically
scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually
by scrolling away from the bottom of the table, and can also be enabled manually
by scrolling to the bottom of the table rows.
# Services
The Open MCT Web platform provides a variety of services which can be retrieved

View File

@ -23,16 +23,16 @@
define([
"./src/directives/MCTTable",
"./src/controllers/RealtimeTableController",
"./src/controllers/HistoricalTableController",
"./src/controllers/RTTelemetryTableController",
"./src/controllers/TelemetryTableController",
"./src/controllers/TableOptionsController",
'../../commonUI/regions/src/Region',
'../../commonUI/browse/src/InspectorRegion',
"legacyRegistry"
], function (
MCTTable,
RealtimeTableController,
HistoricalTableController,
RTTelemetryTableController,
TelemetryTableController,
TableOptionsController,
Region,
InspectorRegion,
@ -109,13 +109,13 @@ define([
],
"controllers": [
{
"key": "HistoricalTableController",
"implementation": HistoricalTableController,
"key": "TelemetryTableController",
"implementation": TelemetryTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
},
{
"key": "RealtimeTableController",
"implementation": RealtimeTableController,
"key": "RTTelemetryTableController",
"implementation": RTTelemetryTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
},
{
@ -130,7 +130,7 @@ define([
"name": "Historical Table",
"key": "table",
"glyph": "\ue604",
"templateUrl": "templates/historical-table.html",
"templateUrl": "templates/table.html",
"needs": [
"telemetry"
],
@ -161,12 +161,6 @@ define([
"key": "table-options-edit",
"templateUrl": "templates/table-options-edit.html"
}
],
"stylesheets": [
{
"stylesheetUrl": "css/table.css",
"priority": "mandatory"
}
]
}
});

View File

@ -1,50 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.sizing-table {
min-width: 100%;
z-index: -1;
visibility: hidden;
position: absolute;
//Add some padding to allow for decorations such as limits indicator
td {
padding-right: 15px;
padding-left: 10px;
white-space: nowrap;
}
}
.mct-table {
table-layout: fixed;
th {
box-sizing: border-box;
}
tbody {
tr {
position: absolute;
}
td {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
}
}
}

View File

@ -1,25 +1,22 @@
<div class="l-view-section scrolling" style="overflow: auto;">
<table class="sizing-table">
<tbody>
<tr>
<td ng-repeat="header in displayHeaders">{{header}}</td>
</tr>
<tr><td ng-repeat="header in displayHeaders" >
{{sizingRow[header].text}}
</td></tr>
</tbody>
</table>
<table class="filterable mct-table"
ng-style="{
<div class="l-view-section scrolling"
ng-style="overrideRowPositioning ?
{'overflow': 'auto'} :
{'overflow': 'scroll'}"
>
<table class="filterable"
ng-style="overrideRowPositioning && {
height: totalHeight + 'px',
'table-layout': overrideRowPositioning ? 'fixed' : 'auto',
'max-width': totalWidth
}">
<thead>
<tr>
<th ng-repeat="header in displayHeaders"
ng-style="{
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}"
ng-class="[
enableSort ? 'sortable' : '',
@ -32,9 +29,11 @@
</tr>
<tr ng-if="enableFilter" class="s-filters">
<th ng-repeat="header in displayHeaders"
ng-style="{
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}">
<input type="text"
ng-model="filters[header]"/>
@ -42,15 +41,21 @@
</tr>
</thead>
<tbody>
<tbody ng-style="overrideRowPositioning ? '' : {
'opacity': '0.0'
}">
<tr ng-repeat="visibleRow in visibleRows track by visibleRow.rowIndex"
ng-style="{
ng-style="overrideRowPositioning && {
position: 'absolute',
top: visibleRow.offsetY + 'px',
}">
<td ng-repeat="header in displayHeaders"
ng-style=" {
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'white-space': 'nowrap',
'max-width': columnWidths[$index] + 'px',
overflow: 'hidden',
'box-sizing': 'border-box'
}"
class="{{visibleRow.contents[header].cssClass}}">
{{ visibleRow.contents[header].text }}

View File

@ -1,4 +1,4 @@
<div ng-controller="RealtimeTableController">
<div ng-controller="RTTelemetryTableController">
<mct-table
headers="headers"
rows="rows"

View File

@ -1,4 +1,4 @@
<div ng-controller="HistoricalTableController">
<div ng-controller="TelemetryTableController">
<mct-table
headers="headers"
rows="rows"

View File

@ -47,7 +47,7 @@ define(
* @param metadata Metadata describing the domains and ranges available
* @returns {TableConfiguration} This object
*/
TableConfiguration.prototype.populateColumns = function (metadata) {
TableConfiguration.prototype.buildColumns = function (metadata) {
var self = this;
this.columns = [];
@ -140,7 +140,8 @@ define(
* @private
*/
TableConfiguration.prototype.defaultColumnConfiguration = function () {
return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
return ((this.domainObject.getModel().configuration || {}).table ||
{}).columns || {};
};
/**
@ -155,16 +156,6 @@ 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.
@ -172,7 +163,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.buildColumnConfiguration = function () {
TableConfiguration.prototype.getColumnConfiguration = function () {
var configuration = {},
//Use existing persisted config, or default it
defaultConfig = this.defaultColumnConfiguration();
@ -188,11 +179,6 @@ define(
defaultConfig[columnTitle];
});
//Synchronize column configuration with model
if (configChanged(configuration, defaultConfig)) {
this.saveColumnConfiguration(configuration);
}
return configuration;
};

View File

@ -1,70 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[
'./TelemetryTableController'
],
function (TableController) {
"use strict";
/**
* 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

@ -23,13 +23,10 @@ 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
@ -61,22 +58,22 @@ define(
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
self.setRows($scope.rows);
self.updateRows($scope.rows);
};
/*
* Define watches to listen for changes to headers and rows.
*/
$scope.$watchCollection('filters', function () {
self.setRows($scope.rows);
self.updateRows($scope.rows);
});
$scope.$watch('headers', this.setHeaders.bind(this));
$scope.$watch('rows', this.setRows.bind(this));
$scope.$watch('headers', this.updateHeaders.bind(this));
$scope.$watch('rows', this.updateRows.bind(this));
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('add:row', this.addRow.bind(this));
$scope.$on('add:row', this.newRow.bind(this));
$scope.$on('remove:row', this.removeRow.bind(this));
}
@ -99,27 +96,23 @@ define(
/**
* Handles a row add event. Rows can be added as needed using the
* `add:row` broadcast event.
* `addRow` broadcast event.
* @private
*/
MCTTableController.prototype.addRow = function (event, rowIndex) {
MCTTableController.prototype.newRow = function (event, rowIndex) {
var row = this.$scope.rows[rowIndex];
//Does the row pass the current filter?
if (this.filterRows([row]).length === 1) {
//Insert the row into the correct position in the array
//Add row to the filtered, sorted list of all rows
if (this.filterRows([row]).length > 0) {
this.insertSorted(this.$scope.displayRows, row);
//Resize the columns , then update the rows visible in the table
this.resize([this.$scope.sizingRow, row])
.then(this.setVisibleRows.bind(this))
.then(this.scrollToBottom.bind(this));
}
this.$timeout(this.setElementSizes.bind(this))
.then(this.scrollToBottom.bind(this));
};
/**
* Handles a row remove event. Rows can be removed as needed using the
* `remove:row` broadcast event.
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* @private
*/
MCTTableController.prototype.removeRow = function (event, rowIndex) {
@ -232,7 +225,7 @@ define(
* enabled, reset filters. If sorting is enabled, reset
* sorting.
*/
MCTTableController.prototype.setHeaders = function (newHeaders) {
MCTTableController.prototype.updateHeaders = function (newHeaders) {
if (!newHeaders){
return;
}
@ -248,7 +241,7 @@ define(
this.$scope.sortColumn = undefined;
this.$scope.sortDirection = undefined;
}
this.setRows(this.$scope.rows);
this.updateRows(this.$scope.rows);
};
/**
@ -256,12 +249,13 @@ define(
* for individual rows.
*/
MCTTableController.prototype.setElementSizes = function () {
var thead = this.thead,
tbody = this.tbody,
var self = this,
thead = this.element.find('thead'),
tbody = this.element.find('tbody'),
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
rowHeight = firstRow.prop('offsetHeight'),
rowHeight = 20,
columnWidth,
tableWidth = 0,
overallHeight = headerHeight + (rowHeight *
@ -278,12 +272,15 @@ define(
this.$scope.headerHeight = headerHeight;
this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight;
this.setVisibleRows();
if (tableWidth > 0) {
this.$scope.totalWidth = tableWidth + 'px';
} else {
this.$scope.totalWidth = 'none';
}
this.$scope.overrideRowPositioning = true;
};
/**
@ -295,14 +292,21 @@ define(
sortKey = this.$scope.sortColumn;
function binarySearch(searchArray, searchElement, min, max){
var sampleAt = Math.floor((max - min) / 2) + min;
var sampleAt = Math.floor((max - min) / 2) + min,
valA,
valB;
if (max < min) {
return min; // Element is not in array, min gives direction
}
switch(self.sortComparator(searchElement[sortKey].text,
searchArray[sampleAt][sortKey].text)) {
valA = isNaN(searchElement[sortKey].text) ?
searchElement[sortKey].text :
parseFloat(searchElement[sortKey].text);
valB = isNaN(searchArray[sampleAt][sortKey].text) ?
searchArray[sampleAt][sortKey].text :
parseFloat(searchArray[sampleAt][sortKey].text);
switch(self.sortComparator(valA, valB)) {
case -1:
return binarySearch(searchArray, searchElement, min,
sampleAt - 1);
@ -340,34 +344,8 @@ define(
*/
MCTTableController.prototype.sortComparator = function (a, b) {
var result = 0,
sortDirectionMultiplier,
numberA,
numberB;
/**
* Given a value, if it is a number, or a string representation of a
* number, then return a number representation. Otherwise, return
* the original value. It's a little more robust than using just
* Number() or parseFloat, or isNaN in isolation, all of which are
* fairly inconsistent in their results.
* @param value The value to return as a number.
* @returns {*} The value cast to a Number, or the original value if
* a Number representation is not possible.
*/
function toNumber (value){
var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value;
return val;
}
sortDirectionMultiplier;
numberA = toNumber(a);
numberB = toNumber(b);
//If they're both numbers, then compare them as numbers
if (typeof numberA === "number" && typeof numberB === "number") {
a = numberA;
b = numberB;
}
//If they're both strings, then ignore case
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
@ -404,7 +382,15 @@ define(
}
return rowsToSort.sort(function (a, b) {
return self.sortComparator(a[sortKey].text, b[sortKey].text);
//If the values to compare can be compared as
// numbers, do so. String comparison of number
// values can cause inconsistencies
var valA = isNaN(a[sortKey].text) ? a[sortKey].text :
parseFloat(a[sortKey].text),
valB = isNaN(b[sortKey].text) ? b[sortKey].text :
parseFloat(b[sortKey].text);
return self.sortComparator(valA, valB);
});
};
@ -414,48 +400,74 @@ define(
* pre-calculate optimal column sizes without having to render
* every row.
*/
MCTTableController.prototype.buildLargestRow = function (rows) {
var largestRow = rows.reduce(function (prevLargest, row) {
MCTTableController.prototype.findLargestRow = function (rows) {
var largestRow = rows.reduce(function (largestRow, row) {
Object.keys(row).forEach(function (key) {
var currentColumn,
currentColumnLength,
largestColumn,
largestColumnLength;
if (row[key]){
currentColumn = (row[key]).text;
var currentColumn = row[key].text,
currentColumnLength =
(currentColumn && currentColumn.length) ?
currentColumn.length :
currentColumn;
largestColumn = prevLargest[key] ? prevLargest[key].text : "";
largestColumnLength = largestColumn.length;
currentColumn,
largestColumn = largestRow[key].text,
largestColumnLength =
(largestColumn && largestColumn.length) ?
largestColumn.length :
largestColumn;
if (currentColumnLength > largestColumnLength) {
prevLargest[key] = JSON.parse(JSON.stringify(row[key]));
}
largestRow[key] = JSON.parse(JSON.stringify(row[key]));
}
});
return prevLargest;
return largestRow;
}, JSON.parse(JSON.stringify(rows[0] || {})));
largestRow = JSON.parse(JSON.stringify(largestRow));
// Pad with characters to accomodate variable-width fonts,
// and remove characters that would allow word-wrapping.
Object.keys(largestRow).forEach(function (key) {
var padCharacters,
i;
largestRow[key].text = String(largestRow[key].text);
padCharacters = largestRow[key].text.length / 10;
for (i = 0; i < padCharacters; i++) {
largestRow[key].text = largestRow[key].text + 'W';
}
largestRow[key].text = largestRow[key].text
.replace(/[ \-_]/g, 'W');
});
return largestRow;
};
/**
* Calculates the widest row in the table, and if necessary, resizes
* the table accordingly
*
* @param rows the rows on which to resize
* @returns {Promise} a promise that will resolve when resizing has
* occurred.
* Calculates the widest row in the table, pads that row, and adds
* it to the table. Allows the table to size itself, then uses this
* as basis for column dimensions.
* @private
*/
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
return this.$timeout(this.setElementSizes.bind(this));
MCTTableController.prototype.resize = function (){
var largestRow = this.findLargestRow(this.$scope.displayRows),
self = this;
this.$scope.visibleRows = [
{
rowIndex: 0,
offsetY: undefined,
contents: largestRow
}
];
//Wait a timeout to allow digest of previous change to visible
// rows to happen.
this.$timeout(function () {
//Remove temporary padding row used for setting column widths
self.$scope.visibleRows = [];
self.setElementSizes();
});
};
/**
* @private
* @priate
*/
MCTTableController.prototype.filterAndSort = function (rows) {
var displayRows = rows;
@ -466,21 +478,26 @@ define(
if (this.$scope.enableSort) {
displayRows = this.sortRows(displayRows.slice(0));
}
return displayRows;
this.$scope.displayRows = displayRows;
};
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.setRows = function (newRows) {
MCTTableController.prototype.updateRows = function (newRows) {
//Reset visible rows because new row data available.
this.$scope.visibleRows = [];
this.$scope.overrideRowPositioning = false;
//Nothing to show because no columns visible
if (!this.$scope.displayHeaders || !newRows) {
if (!this.$scope.displayHeaders) {
return;
}
this.$scope.displayRows = this.filterAndSort(newRows || []);
this.resize(newRows).then(this.setVisibleRows.bind(this));
this.filterAndSort(newRows || []);
this.resize();
};
/**

View File

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

View File

@ -51,29 +51,13 @@ define(
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.listeners = [];
$scope.columnsForm = {};
function unlisten() {
self.listeners.forEach(function (listener) {
listener();
});
}
$scope.$watch('domainObject', function(domainObject) {
unlisten();
self.populateForm(domainObject.getModel());
self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) {
this.domainObject.getCapability('mutation').listen(function (model) {
self.populateForm(model);
}));
});
/**
* Maintain a configuration object on scope that stores column
* configuration. On change, synchronize with object model.
*/
$scope.$watchCollection('configuration.table.columns', function (columns){
if (columns){
self.domainObject.useCapability('mutation', function (model) {
@ -83,11 +67,6 @@ define(
}
});
/**
* Destroy all mutation listeners
*/
$scope.$on('$destroy', unlisten);
}
TableOptionsController.prototype.populateForm = function (model) {
@ -107,7 +86,7 @@ define(
'key': key
});
});
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {}));
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration));
};
return TableOptionsController;

View File

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

View File

@ -12,51 +12,6 @@ define(
* Defines a generic 'Table' component. The table can be populated
* en-masse by setting the rows attribute, or rows can be added as
* needed via a broadcast 'addRow' event.
*
* This directive accepts parameters specifying header and row
* content, as well as some additional options.
*
* Two broadcast events for notifying the table that the rows have
* changed. For performance reasons, the table does not monitor the
* content of `rows` constantly.
* - 'add:row': A $broadcast event that will notify the table that
* a new row has been added to the table.
* eg.
* <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) {

View File

@ -116,10 +116,10 @@ define(
}];
beforeEach(function() {
table.populateColumns(metadata);
table.buildColumns(metadata);
});
it("populates columns", function() {
it("populates the columns attribute", function() {
expect(table.columns.length).toBe(5);
});
@ -141,7 +141,7 @@ define(
it("Provides a default configuration with all columns" +
" visible", function() {
var configuration = table.buildColumnConfiguration();
var configuration = table.getColumnConfiguration();
expect(configuration).toBeDefined();
expect(Object.keys(configuration).every(function(key){
@ -160,7 +160,7 @@ define(
};
mockModel.configuration = modelConfig;
tableConfig = table.buildColumnConfiguration();
tableConfig = table.getColumnConfiguration();
expect(tableConfig).toBeDefined();
expect(tableConfig['Range 1']).toBe(false);

View File

@ -58,18 +58,15 @@ define(
mockElement = jasmine.createSpyObj('element', [
'find',
'prop',
'on'
]);
mockElement.find.andReturn(mockElement);
mockElement.prop.andReturn(0);
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
spyOn(controller, 'setVisibleRows');
});
it('Reacts to changes to filters, headers, and rows', function() {
@ -118,7 +115,7 @@ define(
});
it('Sets rows on scope when rows change', function() {
controller.setRows(testRows);
controller.updateRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
expect(mockScope.displayRows).toEqual(testRows);
});
@ -130,7 +127,7 @@ define(
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
};
controller.setRows(testRows);
controller.updateRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
addRowFunc(undefined, 3);
@ -139,8 +136,10 @@ define(
it('Supports removing rows individually', function() {
var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1];
controller.setRows(testRows);
controller.updateRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
spyOn(controller, 'setVisibleRows');
//controller.setVisibleRows.andReturn(undefined);
removeRowFunc(undefined, 2);
expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled();
@ -180,54 +179,7 @@ define(
expect(sortedRows[2].col2.text).toEqual('abc');
});
it('correctly sorts rows of differing types', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
testRows.push({
'col1': {'text': 'row4 col1'},
'col2': {'text': '123'},
'col3': {'text': 'row4 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': '456'},
'col3': {'text': 'row5 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': ''},
'col3': {'text': 'row5 col3'}
});
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col2.text).toEqual('ghi');
expect(sortedRows[1].col2.text).toEqual('def');
expect(sortedRows[2].col2.text).toEqual('abc');
expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456');
expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123');
expect(sortedRows[sortedRows.length-1].col2.text).toEqual('');
});
describe('The sort comparator', function () {
it('Correctly sorts different data types', function () {
var val1 = "",
val2 = "1",
val3 = "2016-04-05 18:41:30.713Z",
val4 = "1.1",
val5 = "8.945520958175627e-13";
mockScope.sortDirection = "asc";
expect(controller.sortComparator(val1, val2)).toEqual(-1);
expect(controller.sortComparator(val3, val1)).toEqual(1);
expect(controller.sortComparator(val3, val2)).toEqual(1);
expect(controller.sortComparator(val4, val2)).toEqual(1);
expect(controller.sortComparator(val2, val5)).toEqual(1);
});
});
describe('Adding new rows', function () {
describe('Adding new rows', function() {
var row4,
row5,
row6;
@ -258,20 +210,20 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.rows.push(row4);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
//Add a duplicate row
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
@ -287,18 +239,18 @@ define(
mockScope.displayRows = controller.filterRows(testRows);
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
it('Adds new rows at the correct sort position when' +
' not sorted ', function () {
' not sorted ', function() {
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
@ -306,33 +258,14 @@ define(
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length-1);
controller.newRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
it('Resizes columns if length of any columns in new' +
' row exceeds corresponding existing column', function() {
var row7 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'some longer string'},
'col3': {'text': 'row6 col3'}
};
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row7);
controller.addRow(undefined, mockScope.rows.length-1);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
});
});
});

View File

@ -23,7 +23,7 @@
define(
[
"../../src/controllers/RealtimeTableController"
"../../src/controllers/RTTelemetryTableController"
],
function (TableController) {
"use strict";
@ -77,14 +77,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
'populateColumns',
'buildColumnConfiguration',
'buildColumns',
'getColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
mockTable.getRowValues.andReturn(mockTableRow);
mockDomainObject= jasmine.createSpyObj('domainObject', [
@ -107,16 +107,13 @@ define(
'unsubscribe',
'getDatum',
'promiseTelemetryObjects',
'getTelemetryObjects',
'request'
'getTelemetryObjects'
]);
// Arbitrary array with non-zero length, contents are not
// used by mocks
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
mockTelemetryHandle.getDatum.andReturn({});
mockTelemetryHandle.request.andReturn(promise(undefined));
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'

View File

@ -47,36 +47,18 @@ define(
'listen'
]);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability',
'getModel'
'getCapability'
]);
mockDomainObject.getCapability.andReturn(mockCapability);
mockDomainObject.getModel.andReturn({});
mockScope = jasmine.createSpyObj('scope', [
'$watchCollection',
'$watch',
'$on'
'$watchCollection'
]);
mockScope.domainObject = mockDomainObject;
controller = new TableOptionsController(mockScope);
});
it('Listens for changing domain object', function() {
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
});
it('On destruction of controller, destroys listeners', function() {
var unlistenFunc = jasmine.createSpy("unlisten");
controller.listeners.push(unlistenFunc);
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(unlistenFunc).toHaveBeenCalled();
});
it('Registers a listener for mutation events on the object', function() {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockCapability.listen).toHaveBeenCalled();
});

View File

@ -23,7 +23,7 @@
define(
[
"../../src/controllers/HistoricalTableController"
"../../src/controllers/TelemetryTableController"
],
function (TableController) {
"use strict";
@ -73,14 +73,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
'populateColumns',
'buildColumnConfiguration',
'buildColumns',
'getColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
mockDomainObject= jasmine.createSpyObj('domainObject', [
'getCapability',
@ -126,18 +126,21 @@ define(
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
});
describe('makes use of the table', function () {
describe('the controller makes use of the table', function () {
it('to create column definitions from telemetry' +
' metadata', function () {
controller.setup();
expect(mockTable.populateColumns).toHaveBeenCalled();
expect(mockTable.buildColumns).toHaveBeenCalled();
});
it('to create column configuration, which is written to the' +
' object model', function () {
var mockModel = {};
controller.setup();
expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
expect(mockTable.saveColumnConfiguration).toHaveBeenCalled();
});
});