mirror of
https://github.com/nasa/openmct.git
synced 2024-12-20 21:53:08 +00:00
[Tables] #707 Added auto-scroll, addressed race condition in Sinewave and event telemetry providers
Fixed issue with visible padding row Incremental improvements Added tests Added tests for sorted insert, and fixed lint errors
This commit is contained in:
parent
a4eb9d6a94
commit
7da1a218ba
@ -57,6 +57,13 @@ define([
|
||||
},
|
||||
"telemetry": {
|
||||
"source": "eventGenerator",
|
||||
"domains": [
|
||||
{
|
||||
"key": "time",
|
||||
"name": "Time",
|
||||
"format": "utc"
|
||||
}
|
||||
],
|
||||
"ranges": [
|
||||
{
|
||||
"format": "string"
|
||||
|
@ -36,7 +36,9 @@ define(
|
||||
function EventTelemetryProvider($q, $timeout) {
|
||||
var
|
||||
subscriptions = [],
|
||||
genInterval = 1000;
|
||||
genInterval = 1000,
|
||||
generating = false,
|
||||
id = Math.random() * 100000;
|
||||
|
||||
//
|
||||
function matchesSource(request) {
|
||||
@ -78,10 +80,13 @@ define(
|
||||
}
|
||||
|
||||
function startGenerating() {
|
||||
generating = true;
|
||||
$timeout(function () {
|
||||
handleSubscriptions();
|
||||
if (subscriptions.length > 0) {
|
||||
if (generating && subscriptions.length > 0) {
|
||||
startGenerating();
|
||||
} else {
|
||||
generating = false;
|
||||
}
|
||||
}, genInterval);
|
||||
}
|
||||
@ -91,8 +96,6 @@ define(
|
||||
callback: callback,
|
||||
requests: requests
|
||||
};
|
||||
console.log("subscribe... " + Date.now() / 1000 + " request:" +
|
||||
" " + requests[0].id);
|
||||
function unsubscribe() {
|
||||
subscriptions = subscriptions.filter(function (s) {
|
||||
return s !== subscription;
|
||||
@ -100,7 +103,7 @@ define(
|
||||
}
|
||||
|
||||
subscriptions.push(subscription);
|
||||
if (subscriptions.length === 1) {
|
||||
if (!generating) {
|
||||
startGenerating();
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,8 @@ define(
|
||||
* @constructor
|
||||
*/
|
||||
function SinewaveTelemetryProvider($q, $timeout) {
|
||||
var subscriptions = [];
|
||||
var subscriptions = [],
|
||||
generating = false;
|
||||
|
||||
//
|
||||
function matchesSource(request) {
|
||||
@ -75,10 +76,13 @@ define(
|
||||
}
|
||||
|
||||
function startGenerating() {
|
||||
generating = true;
|
||||
$timeout(function () {
|
||||
handleSubscriptions();
|
||||
if (subscriptions.length > 0) {
|
||||
if (generating && subscriptions.length > 0) {
|
||||
startGenerating();
|
||||
} else {
|
||||
generating = false;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@ -97,7 +101,7 @@ define(
|
||||
|
||||
subscriptions.push(subscription);
|
||||
|
||||
if (subscriptions.length === 1) {
|
||||
if (!generating) {
|
||||
startGenerating();
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,8 @@ define([
|
||||
"composition": []
|
||||
},
|
||||
"views": [
|
||||
"realtime"
|
||||
"rt-table",
|
||||
"scrolling-table"
|
||||
]
|
||||
}
|
||||
],
|
||||
@ -137,7 +138,7 @@ define([
|
||||
},
|
||||
{
|
||||
"name": "Real-time Table",
|
||||
"key": "realtime",
|
||||
"key": "rt-table",
|
||||
"glyph": "\ue605",
|
||||
"templateUrl": "templates/rt-table.html",
|
||||
"needs": [
|
||||
|
@ -1,5 +1,7 @@
|
||||
<div class="l-view-section scrolling"
|
||||
style="overflow: auto;"
|
||||
ng-style="overrideRowPositioning ?
|
||||
{'overflow': 'auto'} :
|
||||
{'overflow': 'scroll'}"
|
||||
>
|
||||
<table class="filterable"
|
||||
ng-style="overrideRowPositioning && {
|
||||
@ -59,6 +61,5 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
@ -3,6 +3,7 @@
|
||||
headers="headers"
|
||||
rows="rows"
|
||||
enableFilter="true"
|
||||
enableSort="true">
|
||||
enableSort="true"
|
||||
auto-scroll="autoScroll">
|
||||
</mct-table>
|
||||
</div>
|
9
platform/features/table/res/templates/scrolling.html
Normal file
9
platform/features/table/res/templates/scrolling.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div ng-controller="RTTelemetryTableController">
|
||||
<mct-table
|
||||
headers="headers"
|
||||
rows="rows"
|
||||
enableFilter="true"
|
||||
enableSort="true"
|
||||
auto-scroll="true">
|
||||
</mct-table>
|
||||
</div>
|
@ -54,10 +54,6 @@ define(
|
||||
|
||||
if (metadata) {
|
||||
|
||||
if (metadata.length > 1){
|
||||
self.addColumn(new NameColumn(), 0);
|
||||
}
|
||||
|
||||
metadata.forEach(function (metadatum) {
|
||||
//Push domains first
|
||||
(metadatum.domains || []).forEach(function (domainMetadata) {
|
||||
@ -67,6 +63,10 @@ define(
|
||||
self.addColumn(new RangeColumn(rangeMetadata, self.telemetryFormatter));
|
||||
});
|
||||
});
|
||||
|
||||
if (this.columns.length > 0){
|
||||
self.addColumn(new NameColumn(), 0);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
@ -5,6 +5,15 @@ define(
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 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, $timeout, element) {
|
||||
var self = this;
|
||||
|
||||
@ -12,10 +21,11 @@ define(
|
||||
this.element = element;
|
||||
this.$timeout = $timeout;
|
||||
this.maxDisplayRows = 50;
|
||||
element.find('div').on('scroll', this.setVisibleRows.bind(this));
|
||||
this.scrollable = element.find('div')[0];
|
||||
|
||||
$scope.visibleRows = [];
|
||||
this.scrollable = element.find('div');
|
||||
this.scrollable.on('scroll', this.onScroll.bind(this));
|
||||
|
||||
$scope.visibleRows = [];
|
||||
$scope.overrideRowPositioning = false;
|
||||
|
||||
/**
|
||||
@ -51,27 +61,82 @@ define(
|
||||
self.updateRows($scope.rows);
|
||||
};
|
||||
|
||||
/*
|
||||
* Define watches to listen for changes to headers and rows.
|
||||
*/
|
||||
$scope.$watchCollection('filters', function () {
|
||||
self.updateRows(self.$scope.displayRows);
|
||||
self.updateRows($scope.rows);
|
||||
});
|
||||
$scope.$watchCollection('headers', this.updateHeaders.bind(this));
|
||||
$scope.$watch('headers', this.updateHeaders.bind(this));
|
||||
$scope.$watch('rows', this.updateRows.bind(this));
|
||||
$scope.$on('newRow', this.newRow.bind(this));
|
||||
}
|
||||
|
||||
MCTTableController.prototype.newRow = function (event, newRow) {
|
||||
this.$scope.displayRows.push(newRow);
|
||||
this.filterAndSort(this.$scope.displayRows);
|
||||
this.$timeout(this.setElementSizes(), 0);
|
||||
/*
|
||||
* Listen for rows added individually (eg. for real-time tables)
|
||||
*/
|
||||
$scope.$on('addRow', this.newRow.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-synchronize between data rows and visible rows, based on array
|
||||
* content and scroll state.
|
||||
* If auto-scroll is enabled, this function will scroll to the
|
||||
* bottom of the page
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.scrollToBottom = function() {
|
||||
var self = this;
|
||||
|
||||
//Use timeout to defer execution until next digest when any
|
||||
// pending UI changes have completed, eg. a new row in the table.
|
||||
if (this.$scope.autoScroll) {
|
||||
this.$timeout(function(){
|
||||
self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a row add event. Rows can be added as needed using the
|
||||
* `addRow` broadcast event.
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.newRow = function (event, row) {
|
||||
//Add row to the filtered, sorted list of all rows
|
||||
if (this.filterRows([row]).length > 0) {
|
||||
this.insertSorted(this.$scope.displayRows, row);
|
||||
}
|
||||
|
||||
//Keep 'rows' synchronized as it provides the unsorted,
|
||||
// unfiltered model for this view
|
||||
if (!this.$scope.rows) {
|
||||
this.$scope.rows = [];
|
||||
}
|
||||
this.$scope.rows.push(row);
|
||||
|
||||
this.$timeout(this.setElementSizes.bind(this))
|
||||
.then(this.scrollToBottom.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.onScroll = function (event) {
|
||||
//If user scrolls away from bottom, disable auto-scroll.
|
||||
// Auto-scroll will be re-enabled if user scrolls to bottom again.
|
||||
if (this.scrollable[0].scrollTop < (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) {
|
||||
this.$scope.autoScroll = false;
|
||||
} else {
|
||||
this.$scope.autoScroll = true;
|
||||
}
|
||||
this.setVisibleRows();
|
||||
this.$scope.$digest();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets visible rows based on array
|
||||
* content and current scroll state.
|
||||
*/
|
||||
MCTTableController.prototype.setVisibleRows = function () {
|
||||
var self = this,
|
||||
target = this.scrollable,
|
||||
target = this.scrollable[0],
|
||||
topScroll = target.scrollTop,
|
||||
bottomScroll = topScroll + target.offsetHeight,
|
||||
firstVisible,
|
||||
@ -87,7 +152,7 @@ define(
|
||||
// rows (if data added)
|
||||
if (this.$scope.visibleRows.length != this.$scope.displayRows.length){
|
||||
start = 0;
|
||||
end = this.$scope.displayRows.length-1;
|
||||
end = this.$scope.displayRows.length;
|
||||
} else {
|
||||
//Data is in sync, and no need to calculate scroll,
|
||||
// so do nothing.
|
||||
@ -114,13 +179,12 @@ define(
|
||||
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
//end = this.$scope.visibleRows.length - 1;
|
||||
end = Math.min(this.maxDisplayRows, this.$scope.displayRows.length) - 1;
|
||||
end = Math.min(this.maxDisplayRows, this.$scope.displayRows.length);
|
||||
} else if (end >= this.$scope.displayRows.length) {
|
||||
end = this.$scope.displayRows.length - 1;
|
||||
end = this.$scope.displayRows.length;
|
||||
start = end - this.maxDisplayRows + 1;
|
||||
}
|
||||
if (this.$scope.visibleRows[0].rowIndex === start &&
|
||||
if (this.$scope.visibleRows[0] && this.$scope.visibleRows[0].rowIndex === start &&
|
||||
this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
|
||||
.rowIndex === end) {
|
||||
|
||||
@ -137,8 +201,6 @@ define(
|
||||
contents: row
|
||||
};
|
||||
});
|
||||
|
||||
this.$scope.$digest();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -156,7 +218,7 @@ define(
|
||||
this.$scope.filters = {};
|
||||
}
|
||||
// Reset column sort information unless the new headers
|
||||
// contain the column current sorted on.
|
||||
// contain the column currently sorted on.
|
||||
if (this.$scope.enableSort && newHeaders.indexOf(this.$scope.sortColumn) === -1) {
|
||||
this.$scope.sortColumn = undefined;
|
||||
this.$scope.sortDirection = undefined;
|
||||
@ -175,7 +237,6 @@ define(
|
||||
firstRow = tbody.find('tr'),
|
||||
column = firstRow.find('td'),
|
||||
headerHeight = thead.prop('offsetHeight'),
|
||||
//row height is hard-coded for now.
|
||||
rowHeight = 20,
|
||||
overallHeight = headerHeight + (rowHeight * (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
|
||||
|
||||
@ -192,6 +253,80 @@ define(
|
||||
this.$scope.overrideRowPositioning = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.insertSorted = function(array, element) {
|
||||
var index = -1,
|
||||
self = this,
|
||||
sortKey = this.$scope.sortColumn;
|
||||
|
||||
function binarySearch(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(self.sortComparator(searchElement[sortKey].text, searchArray[sampleAt][sortKey].text)) {
|
||||
case -1:
|
||||
return binarySearch(searchArray, searchElement, min, sampleAt - 1);
|
||||
case 0 :
|
||||
return sampleAt;
|
||||
case 1 :
|
||||
return binarySearch(searchArray, searchElement, sampleAt + 1, max);
|
||||
}
|
||||
}
|
||||
|
||||
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 = binarySearch(array, element, 0, array.length - 1);
|
||||
}
|
||||
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;
|
||||
|
||||
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.
|
||||
@ -199,38 +334,12 @@ define(
|
||||
* Does not modify the array that was passed in.
|
||||
*/
|
||||
MCTTableController.prototype.sortRows = function(rowsToSort) {
|
||||
/**
|
||||
* Compare two variables, returning a number that represents
|
||||
* which is larger. Similar to the default array sort
|
||||
* comparator, but does not coerce all values to string before
|
||||
* conversion. Strings are lowercased before comparison.
|
||||
*/
|
||||
function genericComparator(a, b) {
|
||||
if (typeof a === "string" && typeof b === "string") {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
}
|
||||
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
var self = this,
|
||||
sortKey = this.$scope.sortColumn;
|
||||
|
||||
if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
|
||||
return rowsToSort;
|
||||
}
|
||||
var sortKey = this.$scope.sortColumn,
|
||||
sortDirectionMultiplier;
|
||||
|
||||
if (this.$scope.sortDirection === 'asc') {
|
||||
sortDirectionMultiplier = 1;
|
||||
} else if (this.$scope.sortDirection === 'desc') {
|
||||
sortDirectionMultiplier = -1;
|
||||
}
|
||||
|
||||
return rowsToSort.slice(0).sort(function(a, b) {
|
||||
//If the values to compare can be compared as
|
||||
@ -239,8 +348,7 @@ define(
|
||||
var valA = isNaN(a[sortKey].text) ? a[sortKey].text : parseFloat(a[sortKey].text),
|
||||
valB = isNaN(b[sortKey].text) ? b[sortKey].text : parseFloat(b[sortKey].text);
|
||||
|
||||
return genericComparator(valA, valB) *
|
||||
sortDirectionMultiplier;
|
||||
return self.sortComparator(valA, valB);
|
||||
});
|
||||
};
|
||||
|
||||
@ -271,9 +379,10 @@ define(
|
||||
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.
|
||||
largestRow = JSON.parse(JSON.stringify(largestRow));
|
||||
Object.keys(largestRow).forEach(function(key) {
|
||||
var padCharacters,
|
||||
i;
|
||||
@ -289,8 +398,15 @@ define(
|
||||
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.
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.resize = function (){
|
||||
var largestRow = this.findLargestRow(this.$scope.displayRows);
|
||||
var largestRow = this.findLargestRow(this.$scope.displayRows),
|
||||
self = this;
|
||||
this.$scope.visibleRows = [
|
||||
{
|
||||
rowIndex: 0,
|
||||
@ -299,9 +415,18 @@ define(
|
||||
}
|
||||
];
|
||||
|
||||
this.$timeout(this.setElementSizes.bind(this));
|
||||
//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();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @priate
|
||||
*/
|
||||
MCTTableController.prototype.filterAndSort = function(rows) {
|
||||
var displayRows = rows;
|
||||
if (this.$scope.enableFilter) {
|
||||
@ -319,19 +444,25 @@ define(
|
||||
* 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;
|
||||
|
||||
//Nothing to show because no columns visible
|
||||
if (!this.$scope.displayHeaders) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterAndSort(newRows || []);
|
||||
//Apply filters and sort a copy of the the new rows
|
||||
this.filterAndSort((newRows || []).slice(0));
|
||||
//Resize columns appropriately
|
||||
this.resize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter rows.
|
||||
* Applies user defined filters to rows. These filters are based on
|
||||
* the text entered in the search areas in each column
|
||||
*/
|
||||
MCTTableController.prototype.filterRows = function(rowsToFilter) {
|
||||
var filters = {},
|
||||
|
@ -21,23 +21,16 @@
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* This bundle adds a table view for displaying telemetry data.
|
||||
* @namespace platform/features/table
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'./TelemetryTableController',
|
||||
'../TableConfiguration',
|
||||
'../NameColumn'
|
||||
'./TelemetryTableController'
|
||||
],
|
||||
function (TableController, Table, NameColumn) {
|
||||
function (TableController) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The TableController is responsible for getting data onto the page
|
||||
* in the table widget. This includes handling composition,
|
||||
* configuration, and telemetry subscriptions.
|
||||
* Extends TelemetryTableController and adds real-time streaming
|
||||
* support.
|
||||
* @memberof platform/features/table
|
||||
* @param $scope
|
||||
* @param telemetryHandler
|
||||
@ -46,16 +39,44 @@ define(
|
||||
*/
|
||||
function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
|
||||
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
|
||||
|
||||
$scope.autoScroll = false;
|
||||
|
||||
/*
|
||||
* Determine if auto-scroll should be enabled. Is enabled
|
||||
* automatically when telemetry type is string
|
||||
*/
|
||||
function hasStringTelemetry(domainObject) {
|
||||
var telemetry = domainObject &&
|
||||
domainObject.getCapability('telemetry'),
|
||||
metadata = telemetry ? telemetry.getMetadata() : {},
|
||||
ranges = metadata.ranges || [];
|
||||
|
||||
return ranges.some(function (range) {
|
||||
return range.format === 'string';
|
||||
});
|
||||
}
|
||||
$scope.$watch('domainObject', function(domainObject) {
|
||||
//When a domain object becomes available, check whether the
|
||||
// view should auto-scroll to the bottom.
|
||||
if (domainObject && hasStringTelemetry(domainObject)){
|
||||
$scope.autoScroll = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RTTelemetryTableController.prototype = Object.create(TableController.prototype);
|
||||
|
||||
/**
|
||||
Create a new telemetry subscription.
|
||||
Override the subscribe function defined on the parent controller in
|
||||
order to handle realtime telemetry instead of historical.
|
||||
*/
|
||||
RTTelemetryTableController.prototype.subscribe = function() {
|
||||
console.trace();
|
||||
var self = this;
|
||||
self.$scope.rows = undefined;
|
||||
(this.subscriptions || []).forEach(function(unsubscribe){
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
if (this.handle) {
|
||||
this.handle.unsubscribe();
|
||||
@ -66,11 +87,8 @@ define(
|
||||
self.handle.getTelemetryObjects().forEach(function(telemetryObject){
|
||||
datum = self.handle.getDatum(telemetryObject);
|
||||
if (datum) {
|
||||
if (!self.$scope.rows) {
|
||||
self.$scope.rows = [self.table.getRowValues(telemetryObject, datum)];
|
||||
} else {
|
||||
self.updateRows(telemetryObject, datum);
|
||||
}
|
||||
var rowValue = self.table.getRowValues(telemetryObject, datum);
|
||||
self.$scope.$broadcast('addRow', rowValue);
|
||||
}
|
||||
});
|
||||
|
||||
@ -85,16 +103,6 @@ define(
|
||||
this.setup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add data to rows
|
||||
* @param object The object for which data is available (table may
|
||||
* be composed of multiple objects)
|
||||
* @param datum The data received from the telemetry source
|
||||
*/
|
||||
RTTelemetryTableController.prototype.updateRows = function (object, datum) {
|
||||
this.$scope.$broadcast('newRow', this.table.getRowValues(object, datum));
|
||||
};
|
||||
|
||||
return RTTelemetryTableController;
|
||||
}
|
||||
);
|
||||
|
@ -72,21 +72,22 @@ define(
|
||||
this.$scope.$on("$destroy", this.destroy.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer registration of change listeners until domain object is
|
||||
* available in order to avoid race conditions
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.registerChangeListeners = function() {
|
||||
//Defer registration of change listeners until domain object is
|
||||
// available in order to avoid race conditions
|
||||
|
||||
this.changeListeners.forEach(function (listener) {
|
||||
return listener && listener();
|
||||
});
|
||||
this.changeListeners = [];
|
||||
// When composition changes, re-subscribe to the various
|
||||
// telemetry subscriptions
|
||||
//this.changeListeners.push(this.$scope.$watchCollection('domainObject.getModel().composition', this.subscribe.bind(this)));
|
||||
this.changeListeners.push(this.$scope.$watchCollection('domainObject.getModel().composition', this.subscribe.bind(this)));
|
||||
|
||||
//Change of bounds in time conductor
|
||||
this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', this.subscribe.bind(this)));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@ -100,16 +101,15 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
Create a new subscription. This is called when
|
||||
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() {
|
||||
console.trace();
|
||||
if (this.handle) {
|
||||
this.handle.unsubscribe();
|
||||
}
|
||||
|
||||
this.$scope.rows = [];
|
||||
|
||||
//Noop because not supporting realtime data right now
|
||||
function noop(){
|
||||
}
|
||||
@ -127,12 +127,17 @@ define(
|
||||
|
||||
/**
|
||||
* Add any historical data available
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.addHistoricalData = function(domainObject, series) {
|
||||
var i;
|
||||
var i,
|
||||
newRows = [];
|
||||
|
||||
for (i=0; i < series.getPointCount(); i++) {
|
||||
this.updateRows(domainObject, this.handle.makeDatum(domainObject, series, i));
|
||||
newRows.push(this.table.getRowValues(domainObject, this.handle.makeDatum(domainObject, series, i)));
|
||||
}
|
||||
|
||||
this.$scope.rows = newRows;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -160,7 +165,7 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
* Add data to rows
|
||||
* @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
|
||||
@ -172,6 +177,7 @@ define(
|
||||
/**
|
||||
* When column configuration changes, update the visible headers
|
||||
* accordingly.
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
|
||||
if (!columnConfig){
|
||||
|
@ -1,20 +1,30 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
["../controllers/MCTTableController"],
|
||||
function (MCTTableController) {
|
||||
[
|
||||
"../controllers/MCTTableController",
|
||||
"text!../../res/templates/mct-table.html"
|
||||
],
|
||||
function (MCTTableController, TableTemplate) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @constructor
|
||||
*/
|
||||
function MCTTable($timeout) {
|
||||
return {
|
||||
restrict: "E",
|
||||
templateUrl: "platform/features/table/res/templates/mct-data-table.html",
|
||||
template: TableTemplate,
|
||||
controller: ['$scope', '$timeout', '$element', MCTTableController],
|
||||
scope: {
|
||||
headers: "=",
|
||||
rows: "=",
|
||||
enableFilter: "=?",
|
||||
enableSort: "=?"
|
||||
enableSort: "=?",
|
||||
autoScroll: "=?"
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -120,13 +120,13 @@ define(
|
||||
});
|
||||
|
||||
it("populates the columns attribute", function() {
|
||||
expect(table.columns.length).toBe(4);
|
||||
expect(table.columns.length).toBe(5);
|
||||
});
|
||||
|
||||
it("Build columns populates columns with domains to the left", function() {
|
||||
expect(table.columns[0] instanceof DomainColumn).toBeTruthy();
|
||||
expect(table.columns[1] instanceof DomainColumn).toBeTruthy();
|
||||
expect(table.columns[2] instanceof DomainColumn).toBeFalsy();
|
||||
expect(table.columns[2] instanceof DomainColumn).toBeTruthy();
|
||||
expect(table.columns[3] instanceof DomainColumn).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Produces headers for each column based on title", function() {
|
||||
@ -135,7 +135,7 @@ define(
|
||||
|
||||
spyOn(firstColumn, 'getTitle');
|
||||
headers = table.getHeaders();
|
||||
expect(headers.length).toBe(4);
|
||||
expect(headers.length).toBe(5);
|
||||
expect(firstColumn.getTitle).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -48,6 +48,8 @@ define(
|
||||
watches = {};
|
||||
|
||||
mockScope = jasmine.createSpyObj('scope', [
|
||||
'$watch',
|
||||
'$on',
|
||||
'$watchCollection'
|
||||
]);
|
||||
mockScope.$watchCollection.andCallFake(function(event, callback) {
|
||||
@ -62,14 +64,15 @@ define(
|
||||
|
||||
mockScope.displayHeaders = true;
|
||||
mockTimeout = jasmine.createSpy('$timeout');
|
||||
mockTimeout.andReturn(promise(undefined));
|
||||
|
||||
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
|
||||
});
|
||||
|
||||
it('Reacts to changes to filters, headers, and rows', function() {
|
||||
expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function));
|
||||
expect(mockScope.$watchCollection).toHaveBeenCalledWith('headers', jasmine.any(Function));
|
||||
expect(mockScope.$watchCollection).toHaveBeenCalledWith('rows', jasmine.any(Function));
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith('headers', jasmine.any(Function));
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe('rows', function() {
|
||||
@ -116,6 +119,19 @@ define(
|
||||
expect(mockScope.displayRows).toEqual(testRows);
|
||||
});
|
||||
|
||||
it('Supports adding rows individually', function() {
|
||||
var addRowFunc = mockScope.$on.mostRecentCall.args[1],
|
||||
row4 = {
|
||||
'col1': {'text': 'row3 col1'},
|
||||
'col2': {'text': 'ghi'},
|
||||
'col3': {'text': 'row3 col3'}
|
||||
};
|
||||
controller.updateRows(testRows);
|
||||
expect(mockScope.displayRows.length).toBe(3);
|
||||
addRowFunc(row4);
|
||||
expect(mockScope.displayRows.length).toBe(4);
|
||||
});
|
||||
|
||||
describe('sorting', function() {
|
||||
var sortedRows;
|
||||
|
||||
@ -149,7 +165,87 @@ define(
|
||||
expect(sortedRows[1].col2.text).toEqual('def');
|
||||
expect(sortedRows[2].col2.text).toEqual('abc');
|
||||
});
|
||||
|
||||
describe('Adding new rows', function() {
|
||||
var row4,
|
||||
row5,
|
||||
row6;
|
||||
|
||||
beforeEach(function() {
|
||||
row4 = {
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col2': {'text': 'xyz'},
|
||||
'col3': {'text': 'row5 col3'}
|
||||
};
|
||||
row5 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
'col2': {'text': 'aaa'},
|
||||
'col3': {'text': 'row6 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);
|
||||
|
||||
controller.newRow(undefined, row4);
|
||||
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
|
||||
controller.newRow(undefined, row5);
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
|
||||
controller.newRow(undefined, row6);
|
||||
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
|
||||
|
||||
//Add a duplicate row
|
||||
controller.newRow(undefined, row6);
|
||||
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
|
||||
});
|
||||
|
||||
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);
|
||||
mockScope.displayRows = controller.filterRows(testRows);
|
||||
|
||||
controller.newRow(undefined, row5);
|
||||
expect(mockScope.displayRows.length).toBe(2);
|
||||
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
|
||||
|
||||
controller.newRow(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;
|
||||
|
||||
controller.newRow(undefined, row5);
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
|
||||
controller.newRow(undefined, row6);
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,146 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
|
||||
|
||||
define(
|
||||
[
|
||||
"../../src/controllers/RTTelemetryTableController"
|
||||
],
|
||||
function (TableController) {
|
||||
"use strict";
|
||||
|
||||
describe('The real-time table controller', function() {
|
||||
var mockScope,
|
||||
mockTelemetryHandler,
|
||||
mockTelemetryHandle,
|
||||
mockTelemetryFormatter,
|
||||
mockDomainObject,
|
||||
mockTable,
|
||||
mockConfiguration,
|
||||
watches,
|
||||
mockTableRow,
|
||||
controller;
|
||||
|
||||
function promise(value) {
|
||||
return {
|
||||
then: function (callback){
|
||||
return promise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
watches = {};
|
||||
mockTableRow = {'col1': 'val1', 'col2': 'row2'};
|
||||
|
||||
mockScope = jasmine.createSpyObj('scope', [
|
||||
'$on',
|
||||
'$watch',
|
||||
'$watchCollection',
|
||||
'$broadcast'
|
||||
]);
|
||||
mockScope.$on.andCallFake(function(expression, callback){
|
||||
watches[expression] = callback;
|
||||
});
|
||||
mockScope.$watch.andCallFake(function(expression, callback){
|
||||
watches[expression] = callback;
|
||||
});
|
||||
mockScope.$watchCollection.andCallFake(function(expression, callback){
|
||||
watches[expression] = callback;
|
||||
});
|
||||
|
||||
mockConfiguration = {
|
||||
'range1': true,
|
||||
'range2': true,
|
||||
'domain1': true
|
||||
};
|
||||
|
||||
mockTable = jasmine.createSpyObj('table',
|
||||
[
|
||||
'buildColumns',
|
||||
'getColumnConfiguration',
|
||||
'getRowValues',
|
||||
'saveColumnConfiguration'
|
||||
]
|
||||
);
|
||||
mockTable.columns = [];
|
||||
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
|
||||
mockTable.getRowValues.andReturn(mockTableRow);
|
||||
|
||||
mockDomainObject= jasmine.createSpyObj('domainObject', [
|
||||
'getCapability',
|
||||
'useCapability',
|
||||
'getModel'
|
||||
]);
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockDomainObject.getCapability.andReturn(
|
||||
{
|
||||
getMetadata: function(){
|
||||
return {ranges: [{format: 'string'}]};
|
||||
}
|
||||
});
|
||||
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
|
||||
'getMetadata',
|
||||
'unsubscribe',
|
||||
'getDatum',
|
||||
'promiseTelemetryObjects',
|
||||
'getTelemetryObjects'
|
||||
]);
|
||||
// Arbitrary array with non-zero length, contents are not
|
||||
// used by mocks
|
||||
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
|
||||
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
|
||||
mockTelemetryHandle.getDatum.andReturn({});
|
||||
|
||||
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
|
||||
'handle'
|
||||
]);
|
||||
mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
|
||||
|
||||
controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter);
|
||||
controller.table = mockTable;
|
||||
controller.handle = mockTelemetryHandle;
|
||||
});
|
||||
|
||||
it('registers for streaming telemetry', function() {
|
||||
controller.subscribe();
|
||||
expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true);
|
||||
});
|
||||
|
||||
it('updates table with new streaming telemetry', function() {
|
||||
controller.subscribe();
|
||||
mockTelemetryHandler.handle.mostRecentCall.args[1]();
|
||||
expect(mockScope.$broadcast).toHaveBeenCalledWith('addRow', mockTableRow);
|
||||
});
|
||||
|
||||
it('enables autoscroll for event telemetry', function() {
|
||||
controller.subscribe();
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockScope.autoScroll).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user