-
+
+
+ class="flex-elem l-flex-row no-validate no-margin">
\ No newline at end of file
diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js
index dc687a9edf..18f78930bc 100644
--- a/platform/features/table/src/TableConfiguration.js
+++ b/platform/features/table/src/TableConfiguration.js
@@ -45,26 +45,28 @@ define(
* @param metadata Metadata describing the domains and ranges available
* @returns {TableConfiguration} This object
*/
- TableConfiguration.prototype.buildColumns = function(metadata) {
+ TableConfiguration.prototype.buildColumns = function (metadata) {
var self = this;
this.columns = [];
if (metadata) {
- if (metadata.length > 1){
- self.addColumn(new NameColumn(), 0);
- }
-
metadata.forEach(function (metadatum) {
//Push domains first
(metadatum.domains || []).forEach(function (domainMetadata) {
- self.addColumn(new DomainColumn(domainMetadata, self.telemetryFormatter));
+ self.addColumn(new DomainColumn(domainMetadata,
+ self.telemetryFormatter));
});
(metadatum.ranges || []).forEach(function (rangeMetadata) {
- self.addColumn(new RangeColumn(rangeMetadata, self.telemetryFormatter));
+ self.addColumn(new RangeColumn(rangeMetadata,
+ self.telemetryFormatter));
});
});
+
+ if (this.columns.length > 0){
+ self.addColumn(new NameColumn(), 0);
+ }
}
return this;
};
@@ -96,7 +98,7 @@ define(
* Get a simple list of column titles
* @returns {Array} The titles of the columns
*/
- TableConfiguration.prototype.getHeaders = function() {
+ TableConfiguration.prototype.getHeaders = function () {
var self = this;
return this.columns.map(function (column, i){
return self.getColumnTitle(column) || 'Column ' + (i + 1);
@@ -111,9 +113,9 @@ define(
* @returns {Object} Key value pairs where the key is the column
* title, and the value is the formatted value from the provided datum.
*/
- TableConfiguration.prototype.getRowValues = function(telemetryObject, datum) {
+ TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) {
var self = this;
- return this.columns.reduce(function(rowObject, column, i){
+ return this.columns.reduce(function (rowObject, column, i){
var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1),
columnValue = column.getValue(telemetryObject, datum);
@@ -123,7 +125,9 @@ define(
// Don't replace something with nothing.
// This occurs when there are multiple columns with the
// column title
- if (rowObject[columnTitle] === undefined || rowObject[columnTitle].text === undefined || rowObject[columnTitle].text.length === 0) {
+ if (rowObject[columnTitle] === undefined ||
+ rowObject[columnTitle].text === undefined ||
+ rowObject[columnTitle].text.length === 0) {
rowObject[columnTitle] = columnValue;
}
return rowObject;
@@ -134,7 +138,8 @@ define(
* @private
*/
TableConfiguration.prototype.defaultColumnConfiguration = function () {
- return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
+ return ((this.domainObject.getModel().configuration || {}).table ||
+ {}).columns || {};
};
/**
@@ -156,7 +161,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.getColumnConfiguration = function () {
var configuration = {},
//Use existing persisted config, or default it
defaultConfig = this.defaultColumnConfiguration();
@@ -166,8 +171,10 @@ define(
* specifying whether the column is visible or not. Default to
* existing (persisted) configuration if available
*/
- this.getHeaders().forEach(function(columnTitle) {
- configuration[columnTitle] = typeof defaultConfig[columnTitle] === 'undefined' ? true : defaultConfig[columnTitle];
+ this.getHeaders().forEach(function (columnTitle) {
+ configuration[columnTitle] =
+ typeof defaultConfig[columnTitle] === 'undefined' ? true :
+ defaultConfig[columnTitle];
});
return configuration;
diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js
index 9d106edb2f..ace37bdfe6 100644
--- a/platform/features/table/src/controllers/MCTTableController.js
+++ b/platform/features/table/src/controllers/MCTTableController.js
@@ -3,6 +3,15 @@ define(
[],
function () {
+ /**
+ * A controller for the MCTTable directive. Populates scope with
+ * data used for populating, sorting, and filtering
+ * tables.
+ * @param $scope
+ * @param $timeout
+ * @param element
+ * @constructor
+ */
function MCTTableController($scope, $timeout, element) {
var self = this;
@@ -11,6 +20,9 @@ define(
this.$timeout = $timeout;
this.maxDisplayRows = 50;
+ this.scrollable = element.find('div');
+ this.scrollable.on('scroll', this.onScroll.bind(this));
+
$scope.visibleRows = [];
$scope.overrideRowPositioning = false;
@@ -31,8 +43,6 @@ define(
setDefaults($scope);
- element.find('div').on('scroll', this.onScroll.bind(this));
-
$scope.toggleSort = function (key) {
if (!$scope.enableSort) {
return;
@@ -49,22 +59,96 @@ define(
self.updateRows($scope.rows);
};
+ /*
+ * Define watches to listen for changes to headers and rows.
+ */
$scope.$watchCollection('filters', function () {
- self.updateRows(self.$scope.rows);
+ self.updateRows($scope.rows);
});
- $scope.$watchCollection('headers', this.updateHeaders.bind(this));
- $scope.$watchCollection('rows', this.updateRows.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.newRow.bind(this));
+ $scope.$on('remove:row', this.removeRow.bind(this));
}
/**
- * On scroll, calculate which rows indexes are visible and
- * ensure that an equal number of rows are preloaded for
- * scrolling in either direction.
+ * 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, 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));
+ };
+
+ /**
+ * Handles a row add event. Rows can be added as needed using the
+ * `addRow` broadcast event.
+ * @private
+ */
+ MCTTableController.prototype.removeRow = function (event, rowIndex) {
+ var row = this.$scope.rows[rowIndex],
+ // Do a sequential search here. Only way of finding row is by
+ // object equality, so array is in effect unsorted.
+ indexInDisplayRows = this.$scope.displayRows.indexOf(row);
+ if (indexInDisplayRows != -1) {
+ this.$scope.displayRows.splice(indexInDisplayRows, 1);
+ this.setVisibleRows();
+ }
+ };
+
+ /**
+ * @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,
- topScroll = event.target.scrollTop,
- bottomScroll = topScroll + event.target.offsetHeight,
+ target = this.scrollable[0],
+ topScroll = target.scrollTop,
+ bottomScroll = topScroll + target.offsetHeight,
firstVisible,
lastVisible,
totalVisible,
@@ -72,42 +156,59 @@ define(
start,
end;
+ //No need to scroll
if (this.$scope.displayRows.length < this.maxDisplayRows) {
- return;
- }
-
- if (topScroll < this.$scope.headerHeight) {
- firstVisible = 0;
+ //Check whether need to resynchronize visible with display
+ // rows (if data added)
+ if (this.$scope.visibleRows.length !=
+ this.$scope.displayRows.length){
+ start = 0;
+ end = this.$scope.displayRows.length;
+ } else {
+ //Data is in sync, and no need to calculate scroll,
+ // so do nothing.
+ return;
+ }
} else {
- firstVisible = Math.floor(
- (topScroll - this.$scope.headerHeight) / this.$scope.rowHeight
+ //rows has exceeded display maximum, so may be necessary to
+ // scroll
+ if (topScroll < this.$scope.headerHeight) {
+ firstVisible = 0;
+ } else {
+ firstVisible = Math.floor(
+ (topScroll - this.$scope.headerHeight) /
+ this.$scope.rowHeight
+ );
+ }
+ lastVisible = Math.ceil(
+ (bottomScroll - this.$scope.headerHeight) /
+ this.$scope.rowHeight
);
+
+ totalVisible = lastVisible - firstVisible;
+ numberOffscreen = this.maxDisplayRows - totalVisible;
+ start = firstVisible - Math.floor(numberOffscreen / 2);
+ end = lastVisible + Math.ceil(numberOffscreen / 2);
+
+ if (start < 0) {
+ start = 0;
+ end = Math.min(this.maxDisplayRows,
+ this.$scope.displayRows.length);
+ } else if (end >= this.$scope.displayRows.length) {
+ end = this.$scope.displayRows.length;
+ start = end - this.maxDisplayRows + 1;
+ }
+ if (this.$scope.visibleRows[0] &&
+ this.$scope.visibleRows[0].rowIndex === start &&
+ this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
+ .rowIndex === end) {
+
+ return; // don't update if no changes are required.
+ }
}
- lastVisible = Math.ceil(
- (bottomScroll - this.$scope.headerHeight) / this.$scope.rowHeight
- );
-
- totalVisible = lastVisible - firstVisible;
- numberOffscreen = this.maxDisplayRows - totalVisible;
- start = firstVisible - Math.floor(numberOffscreen / 2);
- end = lastVisible + Math.ceil(numberOffscreen / 2);
-
- if (start < 0) {
- start = 0;
- end = this.$scope.visibleRows.length - 1;
- } else if (end >= this.$scope.displayRows.length) {
- end = this.$scope.displayRows.length - 1;
- start = end - this.maxDisplayRows + 1;
- }
- if (this.$scope.visibleRows[0].rowIndex === start &&
- this.$scope.visibleRows[this.$scope.visibleRows.length-1]
- .rowIndex === end) {
-
- return; // don't update if no changes are required.
- }
-
+ //Set visible rows from display rows, based on calculated offset.
this.$scope.visibleRows = this.$scope.displayRows.slice(start, end)
- .map(function(row, i) {
+ .map(function (row, i) {
return {
rowIndex: start + i,
offsetY: ((start + i) * self.$scope.rowHeight) +
@@ -115,8 +216,6 @@ define(
contents: row
};
});
-
- this.$scope.$digest();
};
/**
@@ -134,10 +233,11 @@ define(
this.$scope.filters = {};
}
// Reset column sort information unless the new headers
- // contain the column current sorted on.
- if (this.$scope.enableSort && newHeaders.indexOf(this.$scope.sortColumn) === -1) {
- this.$scope.sortColumn = undefined;
- this.$scope.sortDirection = undefined;
+ // contain the column currently sorted on.
+ if (this.$scope.enableSort &&
+ newHeaders.indexOf(this.$scope.sortColumn) === -1) {
+ this.$scope.sortColumn = undefined;
+ this.$scope.sortDirection = undefined;
}
this.updateRows(this.$scope.rows);
};
@@ -153,80 +253,142 @@ 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));
+ columnWidth,
+ tableWidth = 0,
+ overallHeight = headerHeight + (rowHeight *
+ (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
this.$scope.columnWidths = [];
while (column.length) {
+ columnWidth = column.prop('offsetWidth');
this.$scope.columnWidths.push(column.prop('offsetWidth'));
+ tableWidth += columnWidth;
column = column.next();
}
this.$scope.headerHeight = headerHeight;
this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight;
+ this.setVisibleRows();
- this.$scope.visibleRows = this.$scope.displayRows.slice(0, this.maxDisplayRows).map(function(row, i) {
- return {
- rowIndex: i,
- offsetY: (i * self.$scope.rowHeight) + self.$scope.headerHeight,
- contents: row
- };
- });
+ if (tableWidth > 0) {
+ this.$scope.totalWidth = tableWidth + 'px';
+ } else {
+ this.$scope.totalWidth = 'none';
+ }
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,
+ valA,
+ valB;
+ 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)) {
+ 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.
*
* 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;
- }
+ MCTTableController.prototype.sortRows = function (rowsToSort) {
+ 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) {
+ 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);
+ 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);
});
};
@@ -236,7 +398,7 @@ define(
* pre-calculate optimal column sizes without having to render
* every row.
*/
- MCTTableController.prototype.findLargestRow = function(rows) {
+ MCTTableController.prototype.findLargestRow = function (rows) {
var largestRow = rows.reduce(function (largestRow, row) {
Object.keys(row).forEach(function (key) {
var currentColumn = row[key].text,
@@ -257,10 +419,11 @@ 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) {
+ Object.keys(largestRow).forEach(function (key) {
var padCharacters,
i;
@@ -275,8 +438,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,
@@ -285,7 +455,28 @@ 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) {
+ displayRows = this.filterRows(displayRows);
+ }
+
+ if (this.$scope.enableSort) {
+ displayRows = this.sortRows(displayRows.slice(0));
+ }
+ this.$scope.displayRows = displayRows;
};
/**
@@ -293,29 +484,27 @@ define(
* will be sorted before display.
*/
MCTTableController.prototype.updateRows = function (newRows) {
- var displayRows = 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;
}
- if (this.$scope.enableFilter) {
- displayRows = this.filterRows(displayRows);
- }
-
- if (this.$scope.enableSort) {
- displayRows = this.sortRows(displayRows);
- }
- this.$scope.displayRows = displayRows;
+ this.filterAndSort(newRows || []);
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.
+ * @param rowsToFilter {Object[]} The rows to apply filters to
+ * @returns {Object[]} A filtered copy of the supplied rows
*/
- MCTTableController.prototype.filterRows = function(rowsToFilter) {
+ MCTTableController.prototype.filterRows = function (rowsToFilter) {
var filters = {},
self = this;
@@ -323,7 +512,7 @@ define(
* Returns true if row matches all filters.
*/
function matchRow(filters, row) {
- return Object.keys(filters).every(function(key) {
+ return Object.keys(filters).every(function (key) {
if (!row[key]) {
return false;
}
@@ -336,7 +525,7 @@ define(
return rowsToFilter;
}
- Object.keys(this.$scope.filters).forEach(function(key) {
+ Object.keys(this.$scope.filters).forEach(function (key) {
if (!self.$scope.filters[key]) {
return;
}
diff --git a/platform/features/table/src/controllers/RTTelemetryTableController.js b/platform/features/table/src/controllers/RTTelemetryTableController.js
new file mode 100644
index 0000000000..8a61d61b5e
--- /dev/null
+++ b/platform/features/table/src/controllers/RTTelemetryTableController.js
@@ -0,0 +1,123 @@
+/*****************************************************************************
+ * 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 RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
+ TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
+
+ $scope.autoScroll = false;
+ this.maxRows = 100000;
+
+ /*
+ * 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);
+
+ /**
+ Override the subscribe function defined on the parent controller in
+ order to handle realtime telemetry instead of historical.
+ */
+ 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;
+ 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);
+ }
+ }
+ });
+
+ }
+
+ this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
+ this.$scope.domainObject,
+ updateData,
+ true // Lossless
+ );
+
+ this.setup();
+ };
+
+ return RTTelemetryTableController;
+ }
+);
diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js
index bedaafdde8..b49ca8b0a2 100644
--- a/platform/features/table/src/controllers/TableOptionsController.js
+++ b/platform/features/table/src/controllers/TableOptionsController.js
@@ -56,9 +56,9 @@ define(
self.populateForm(model);
});
- $scope.$watchCollection('configuration.table.columns', function(columns){
+ $scope.$watchCollection('configuration.table.columns', function (columns){
if (columns){
- self.domainObject.useCapability('mutation', function(model) {
+ self.domainObject.useCapability('mutation', function (model) {
model.configuration.table.columns = columns;
});
self.domainObject.getCapability('persistence').persist();
diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js
index f7fcdec74a..1954488a69 100644
--- a/platform/features/table/src/controllers/TelemetryTableController.js
+++ b/platform/features/table/src/controllers/TelemetryTableController.js
@@ -52,10 +52,11 @@ define(
this.handle = undefined;
//this.pending = false;
this.telemetryHandler = telemetryHandler;
- this.table = new TableConfiguration($scope.domainObject, telemetryFormatter);
+ 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(domainObject){
@@ -71,21 +72,24 @@ define(
this.$scope.$on("$destroy", this.destroy.bind(this));
}
- TelemetryTableController.prototype.registerChangeListeners = function() {
- //Defer registration of change listeners until domain object is
- // available in order to avoid race conditions
-
+ /**
+ * 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 = [];
// 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)));
-
+ this.changeListeners.push(this.$scope.$on('telemetry:display:bounds',
+ this.subscribe.bind(this)));
};
/**
@@ -99,16 +103,17 @@ 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() {
+ TelemetryTableController.prototype.subscribe = function () {
+ var self = this;
if (this.handle) {
this.handle.unsubscribe();
}
- this.$scope.rows = [];
-
//Noop because not supporting realtime data right now
function noop(){
}
@@ -119,25 +124,37 @@ define(
true // Lossless
);
- this.handle.request({}, this.addHistoricalData.bind(this));
+ this.handle.request({}).then(this.addHistoricalData.bind(this));
this.setup();
};
/**
- * Add any historical data available
+ * Populates historical data on scope when it becomes available
+ * @private
*/
- TelemetryTableController.prototype.addHistoricalData = function(domainObject, series) {
- var i;
- for (i=0; i < series.getPointCount(); i++) {
- this.updateRows(domainObject, this.handle.makeDatum(domainObject, series, i));
- }
+ 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
*/
- TelemetryTableController.prototype.setup = function() {
+ TelemetryTableController.prototype.setup = function () {
var handle = this.handle,
table = this.table,
self = this;
@@ -159,7 +176,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
@@ -171,6 +188,7 @@ define(
/**
* When column configuration changes, update the visible headers
* accordingly.
+ * @private
*/
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
if (!columnConfig){
@@ -178,7 +196,7 @@ define(
this.table.saveColumnConfiguration(columnConfig);
}
//Populate headers with visible columns (determined by configuration)
- this.$scope.headers = Object.keys(columnConfig).filter(function(column) {
+ this.$scope.headers = Object.keys(columnConfig).filter(function (column) {
return columnConfig[column];
});
};
diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js
index c7feb6bc35..c5f16329e5 100644
--- a/platform/features/table/src/directives/MCTTable.js
+++ b/platform/features/table/src/directives/MCTTable.js
@@ -1,19 +1,49 @@
+/*****************************************************************************
+ * 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(
- ["../controllers/MCTTableController"],
- function (MCTTableController) {
-
- function MCTTable() {
+ [
+ "../controllers/MCTTableController",
+ "text!../../res/templates/mct-table.html"
+ ],
+ function (MCTTableController, TableTemplate) {
+ /**
+ * Defines a generic 'Table' component. The table can be populated
+ * en-masse by setting the rows attribute, or rows can be added as
+ * needed via a broadcast 'addRow' event.
+ * @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: "=?"
+ },
};
}
diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js
index 00562c76c6..c3bb3747cd 100644
--- a/platform/features/table/test/TableConfigurationSpec.js
+++ b/platform/features/table/test/TableConfigurationSpec.js
@@ -118,13 +118,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() {
@@ -133,7 +133,7 @@ define(
spyOn(firstColumn, 'getTitle');
headers = table.getHeaders();
- expect(headers.length).toBe(4);
+ expect(headers.length).toBe(5);
expect(firstColumn.getTitle).toHaveBeenCalled();
});
diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js
index 4fe4be7df3..077511de36 100644
--- a/platform/features/table/test/controllers/MCTTableControllerSpec.js
+++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js
@@ -34,10 +34,20 @@ define(
mockTimeout,
mockElement;
+ function promise(value) {
+ return {
+ then: function (callback) {
+ return promise(callback(value));
+ }
+ };
+ }
+
beforeEach(function() {
watches = {};
mockScope = jasmine.createSpyObj('scope', [
+ '$watch',
+ '$on',
'$watchCollection'
]);
mockScope.$watchCollection.andCallFake(function(event, callback) {
@@ -52,14 +62,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() {
@@ -82,6 +93,7 @@ define(
'col3': {'text': 'row3 col3'}
}
];
+ mockScope.rows = testRows;
});
it('Filters results based on filter input', function() {
@@ -106,6 +118,31 @@ define(
expect(mockScope.displayRows).toEqual(testRows);
});
+ it('Supports adding rows individually', function() {
+ var addRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-2].args[1],
+ row4 = {
+ 'col1': {'text': 'row3 col1'},
+ 'col2': {'text': 'ghi'},
+ 'col3': {'text': 'row3 col3'}
+ };
+ controller.updateRows(testRows);
+ expect(mockScope.displayRows.length).toBe(3);
+ testRows.push(row4);
+ addRowFunc(undefined, 3);
+ expect(mockScope.displayRows.length).toBe(4);
+ });
+
+ it('Supports removing rows individually', function() {
+ var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1];
+ 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();
+ });
+
describe('sorting', function() {
var sortedRows;
@@ -139,7 +176,98 @@ 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.slice(0));
+
+ mockScope.rows.push(row4);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
+
+ mockScope.rows.push(row5);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
+
+ mockScope.rows.push(row6);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
+
+ //Add a duplicate row
+ mockScope.rows.push(row6);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ 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.slice(0));
+ mockScope.displayRows = controller.filterRows(testRows);
+
+ mockScope.rows.push(row5);
+ 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.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() {
+ mockScope.sortColumn = undefined;
+ mockScope.sortDirection = undefined;
+ mockScope.filters = {};
+
+ mockScope.displayRows = testRows.slice(0);
+
+ mockScope.rows.push(row5);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
+
+ mockScope.rows.push(row6);
+ controller.newRow(undefined, mockScope.rows.length-1);
+ expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
+ });
+
+ });
});
+
+
});
});
});
diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js
new file mode 100644
index 0000000000..59911d1771
--- /dev/null
+++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js
@@ -0,0 +1,166 @@
+/*****************************************************************************
+ * 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',
+ '$digest',
+ '$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);
+ });
+
+ describe('receives new telemetry', function () {
+ beforeEach(function() {
+ controller.subscribe();
+ mockScope.rows = [];
+ });
+
+ it('updates table with new streaming telemetry', function () {
+ mockTelemetryHandler.handle.mostRecentCall.args[1]();
+ expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0);
+ });
+ it('observes the row limit', function () {
+ var i = 0;
+ controller.maxRows = 10;
+
+ //Fill rows array with elements
+ for (; i < 10; i++) {
+ mockScope.rows.push({row: i});
+ }
+ mockTelemetryHandler.handle.mostRecentCall.args[1]();
+ expect(mockScope.rows.length).toBe(controller.maxRows);
+ expect(mockScope.rows[mockScope.rows.length-1]).toBe(mockTableRow);
+ expect(mockScope.rows[0].row).toBe(1);
+ });
+ });
+
+ it('enables autoscroll for event telemetry', function () {
+ controller.subscribe();
+ mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
+ expect(mockScope.autoScroll).toBe(true);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
index 91e6a05315..c99e7e638e 100644
--- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
+++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
@@ -26,7 +26,7 @@ define(
],
function (TableController) {
- describe('The Table Controller', function() {
+ describe('The Table Controller', function () {
var mockScope,
mockTelemetryHandler,
mockTelemetryHandle,
@@ -45,7 +45,7 @@ define(
};
}
- beforeEach(function() {
+ beforeEach(function () {
watches = {};
mockScope = jasmine.createSpyObj('scope', [
'$on',
@@ -53,13 +53,13 @@ define(
'$watchCollection'
]);
- mockScope.$on.andCallFake(function(expression, callback){
+ mockScope.$on.andCallFake(function (expression, callback){
watches[expression] = callback;
});
- mockScope.$watch.andCallFake(function(expression, callback){
+ mockScope.$watch.andCallFake(function (expression, callback){
watches[expression] = callback;
});
- mockScope.$watchCollection.andCallFake(function(expression, callback){
+ mockScope.$watchCollection.andCallFake(function (expression, callback){
watches[expression] = callback;
});
@@ -92,11 +92,15 @@ define(
mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
'request',
'promiseTelemetryObjects',
+ 'getTelemetryObjects',
'getMetadata',
+ 'getSeries',
'unsubscribe',
'makeDatum'
]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
+ mockTelemetryHandle.request.andReturn(promise(undefined));
+ mockTelemetryHandle.getTelemetryObjects.andReturn([]);
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'
@@ -108,60 +112,63 @@ define(
controller.handle = mockTelemetryHandle;
});
- it('subscribes to telemetry handler for telemetry updates', function() {
+ it('subscribes to telemetry handler for telemetry updates', function () {
controller.subscribe();
expect(mockTelemetryHandler.handle).toHaveBeenCalled();
expect(mockTelemetryHandle.request).toHaveBeenCalled();
});
- it('Unsubscribes from telemetry when scope is destroyed',function() {
+ it('Unsubscribes from telemetry when scope is destroyed',function () {
controller.handle = mockTelemetryHandle;
watches.$destroy();
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
});
- describe('the controller makes use of the table', function() {
+ describe('the controller makes use of the table', function () {
it('to create column definitions from telemetry' +
- ' metadata', function() {
+ ' metadata', function () {
controller.setup();
expect(mockTable.buildColumns).toHaveBeenCalled();
});
it('to create column configuration, which is written to the' +
- ' object model', function() {
+ ' object model', function () {
controller.setup();
expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
expect(mockTable.saveColumnConfiguration).toHaveBeenCalled();
});
});
- it('updates the rows on scope when historical telemetry is received', function(){
+ it('updates the rows on scope when historical telemetry is received', function (){
var mockSeries = {
- getPointCount: function() {
+ getPointCount: function () {
return 5;
},
- getDomainValue: function() {
+ getDomainValue: function () {
return 'Domain Value';
},
- getRangeValue: function() {
+ getRangeValue: function () {
return 'Range Value';
}
},
mockRow = {'domain': 'Domain Value', 'range': 'Range' +
' Value'};
- mockTelemetryHandle.makeDatum.andCallFake(function(){
+ mockTelemetryHandle.makeDatum.andCallFake(function (){
return mockRow;
});
mockTable.getRowValues.andReturn(mockRow);
+ mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
+ mockTelemetryHandle.getSeries.andReturn(mockSeries);
+
controller.addHistoricalData(mockDomainObject, mockSeries);
expect(controller.$scope.rows.length).toBe(5);
expect(controller.$scope.rows[0]).toBe(mockRow);
});
- it('filters the visible columns based on configuration', function(){
+ it('filters the visible columns based on configuration', function (){
controller.filterColumns();
expect(controller.$scope.headers.length).toBe(3);
expect(controller.$scope.headers[2]).toEqual('domain1');
@@ -172,14 +179,14 @@ define(
expect(controller.$scope.headers[2]).toBeUndefined();
});
- describe('creates event listeners', function(){
- beforeEach(function() {
+ describe('creates event listeners', function (){
+ beforeEach(function () {
spyOn(controller,'subscribe');
spyOn(controller, 'filterColumns');
});
it('triggers telemetry subscription update when domain' +
- ' object changes', function() {
+ ' object changes', function () {
controller.registerChangeListeners();
//'watches' object is populated by fake scope watch and
// watchCollection functions defined above
@@ -189,7 +196,7 @@ define(
});
it('triggers telemetry subscription update when domain' +
- ' object composition changes', function() {
+ ' object composition changes', function () {
controller.registerChangeListeners();
expect(watches['domainObject.getModel().composition']).toBeDefined();
watches['domainObject.getModel().composition']();
@@ -197,7 +204,7 @@ define(
});
it('triggers telemetry subscription update when time' +
- ' conductor bounds change', function() {
+ ' conductor bounds change', function () {
controller.registerChangeListeners();
expect(watches['telemetry:display:bounds']).toBeDefined();
watches['telemetry:display:bounds']();
@@ -205,7 +212,7 @@ define(
});
it('triggers refiltering of the columns when configuration' +
- ' changes', function() {
+ ' changes', function () {
controller.setup();
expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined();
watches['domainObject.getModel().configuration.table.columns']();
diff --git a/platform/features/timeline/bundle.js b/platform/features/timeline/bundle.js
index dcae27e884..dda2080c71 100644
--- a/platform/features/timeline/bundle.js
+++ b/platform/features/timeline/bundle.js
@@ -21,6 +21,7 @@
*****************************************************************************/
define([
+ "./src/actions/ExportTimelineAsCSVAction",
"./src/controllers/TimelineController",
"./src/controllers/TimelineGraphController",
"./src/controllers/TimelineDateTimeController",
@@ -49,6 +50,7 @@ define([
"text!./res/templates/controls/datetime.html",
'legacyRegistry'
], function (
+ ExportTimelineAsCSVAction,
TimelineController,
TimelineGraphController,
TimelineDateTimeController,
@@ -83,6 +85,15 @@ define([
"description": "Resources, templates, CSS, and code for Timelines.",
"resources": "res",
"extensions": {
+ "actions": [
+ {
+ "key": "timeline.export",
+ "name": "Export Timeline as CSV",
+ "category": "contextual",
+ "implementation": ExportTimelineAsCSVAction,
+ "depends": [ "exportService", "notificationService" ]
+ }
+ ],
"constants": [
{
"key": "TIMELINE_MINIMUM_DURATION",
@@ -126,8 +137,9 @@ define([
{
"key": "timeline",
"name": "Timeline",
- "glyph": "S",
- "description": "A container for arranging Timelines and Activities in time.",
+ "glyph": "\u0053",
+ "description": "A time-oriented container that lets you enclose and organize other Timelines and Activities. The Timeline view provides both tabular and Gantt views as well as resource utilization graphing of Activities.",
+ "priority": 502,
"features": [
"creation"
],
@@ -159,20 +171,24 @@ define([
}
],
"model": {
- "composition": []
+ "composition": [],
+ "start": {
+ "timestamp": 0
+ }
}
},
{
"key": "activity",
"name": "Activity",
- "glyph": "a",
+ "glyph": "\u0061",
"features": [
"creation"
],
"contains": [
"activity"
],
- "description": "An action that takes place in time. You can define a start time and duration. Activities can be nested within other Activities, or within Timelines.",
+ "description": "An event or process that starts and ends at a discrete datetime. Activities can be nested in other Activities, and can be added to Timelines. Activity Modes can be added to an Activity to define its resource utilization over time.",
+ "priority": 501,
"properties": [
{
"name": "Start date/time",
@@ -198,17 +214,24 @@ define([
"composition": [],
"relationships": {
"modes": []
+ },
+ "start": {
+ "timestamp": 0
+ },
+ "duration": {
+ "timestamp": 0
}
}
},
{
"key": "mode",
"name": "Activity Mode",
- "glyph": "A",
+ "glyph": "\u0041",
"features": [
"creation"
],
- "description": "Define resource utilizations over time, then apply to an Activity.",
+ "description": "When a sub-system utilizes Power or Communications resources over time, you can define those values in an Activity Mode. Activity Modes can then be linked to Activities to allow resource utilization graphing and estimating in a Timeline.",
+ "priority": 500,
"model": {
"resources": {
"comms": 0,
@@ -243,7 +266,7 @@ define([
{
"key": "values",
"name": "Values",
- "glyph": "A",
+ "glyph": "\u0041",
"template": valuesTemplate,
"type": "mode",
"uses": [
@@ -254,9 +277,9 @@ define([
{
"key": "timeline",
"name": "Timeline",
- "glyph": "S",
+ "glyph": "\u0053",
"type": "timeline",
- "description": "A timeline view of Timelines and Activities.",
+ "description": "A time-oriented container that lets you enclose and organize other Timelines and Activities. The Timeline view provides both tabular and Gantt views as well as resource utilization graphing of Activities.",
"template": timelineTemplate,
"editable": true,
"toolbar": {
@@ -265,18 +288,18 @@ define([
"items": [
{
"method": "add",
- "glyph": "+",
+ "glyph": "\u002b",
"control": "menu-button",
"text": "Add",
"options": [
{
"name": "Timeline",
- "glyph": "S",
+ "glyph": "\u0053",
"key": "timeline"
},
{
"name": "Activity",
- "glyph": "a",
+ "glyph": "\u0061",
"key": "activity"
}
]
@@ -286,37 +309,39 @@ define([
{
"items": [
{
- "glyph": "é",
- "description": "Graph resource utilization",
+ "glyph": "\u00e9",
+ "description": "Graph Resource Utilization",
"control": "button",
"method": "toggleGraph"
},
{
- "glyph": "A",
+ "glyph": "\u0041",
"control": "dialog-button",
"description": "Apply Activity Modes...",
"title": "Apply Activity Modes",
"dialog": {
"control": "selector",
"name": "Modes",
- "type": "mode"
+ "type": "mode",
+ "layout": "controls-under"
},
"property": "modes"
},
{
- "glyph": "è",
+ "glyph": "\u00e8",
"description": "Edit Activity Link",
"title": "Activity Link",
"control": "dialog-button",
"dialog": {
"control": "textfield",
"name": "Link",
- "pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$"
+ "pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
+ "cssclass": "l-input-lg"
},
"property": "link"
},
{
- "glyph": "G",
+ "glyph": "\u0047",
"description": "Edit Properties...",
"control": "button",
"method": "properties"
@@ -327,9 +352,9 @@ define([
"items": [
{
"method": "remove",
- "description": "Remove item",
+ "description": "Remove Item",
"control": "button",
- "glyph": "Z"
+ "glyph": "\u005a"
}
]
}
diff --git a/platform/features/timeline/res/sass/_activities.scss b/platform/features/timeline/res/sass/_activities.scss
index 268c602ae8..5e729432f7 100644
--- a/platform/features/timeline/res/sass/_activities.scss
+++ b/platform/features/timeline/res/sass/_activities.scss
@@ -59,7 +59,7 @@
.handle {
cursor: col-resize;
&.mid {
- cursor: ew-resize;
+ cursor: move;
}
}
}
\ No newline at end of file
diff --git a/platform/features/timeline/res/sass/_timeline-thematic.scss b/platform/features/timeline/res/sass/_timeline-thematic.scss
index be25f24c1d..967a07462f 100644
--- a/platform/features/timeline/res/sass/_timeline-thematic.scss
+++ b/platform/features/timeline/res/sass/_timeline-thematic.scss
@@ -140,17 +140,3 @@
}
}
}
-
-.edit-mode .s-swimlane,
-.s-status-editing .s-swimlane {
- cursor: pointer;
- .t-object-label {
- border-radius: $controlCr;
- cursor: move;
- padding: 2px 5px;
- &:hover {
- background: rgba($colorBodyFg, 0.3);
- color: pullForward($colorBodyFg, 20%);
- }
- }
-}
diff --git a/platform/features/timeline/res/sass/_timelines.scss b/platform/features/timeline/res/sass/_timelines.scss
index dceaa49f9b..0348839263 100644
--- a/platform/features/timeline/res/sass/_timelines.scss
+++ b/platform/features/timeline/res/sass/_timelines.scss
@@ -23,6 +23,11 @@
.l-timeline-holder {
@include absPosDefault();
+ .l-header {
+ @include user-select(none);
+ cursor: default;
+ }
+
.l-timeline-pane {
@include absPosDefault();
@@ -33,6 +38,9 @@
.l-swimlanes-holder {
@include absPosDefault();
top: $timelineTopPaneHeaderH + 1;
+ .l-col.l-plot-resource {
+ cursor: pointer;
+ }
}
// Overall layout
@@ -250,21 +258,15 @@
&.l-plot-resource {
border-left: none !important;
- cursor: pointer;
padding-left: 0;
}
&.l-title {
width: $timelineColTitleW;
- .rep-object-label[draggable="true"] {
+ .rep-object-label {
border-radius: $basicCr;
- @include transition(background-color, 0.25s);
- cursor: pointer;
display: inline-block;
- padding: 0 $interiorMarginSm;
- &:hover {
- background-color: $colorItemTreeHoverBg;
- }
+ padding: 0 $interiorMargin;
}
}
@@ -314,3 +316,11 @@
height: 5px
}
}
+
+.s-status-editing .l-title .rep-object-label[draggable="true"] {
+ @include transition(background-color, 0.25s);
+ cursor: pointer;
+ &:hover {
+ background-color: $colorItemTreeHoverBg;
+ }
+}
\ No newline at end of file
diff --git a/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html b/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html
index 94c20d2ace..e5ca69e15c 100644
--- a/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html
+++ b/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html
@@ -29,9 +29,11 @@
}">
+ ng-click="ngModel.toggleGraph()"
+ title="Click to enable or disable inclusion in Resource Graphing">
+ ng-show="ngModel.graph()"
+ >
é
diff --git a/platform/features/timeline/res/templates/timeline.html b/platform/features/timeline/res/templates/timeline.html
index 3f8acf1073..88903a7011 100644
--- a/platform/features/timeline/res/templates/timeline.html
+++ b/platform/features/timeline/res/templates/timeline.html
@@ -26,20 +26,17 @@
+ class="abs horizontal split-pane-component l-timeline-pane l-pane-l t-pane-v">
-
+
@@ -54,9 +51,7 @@
-
+
diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html
index edd4f81d1c..2f8b266e08 100644
--- a/platform/forms/res/templates/form.html
+++ b/platform/forms/res/templates/form.html
@@ -31,7 +31,9 @@
req: row.required,
valid: mctFormInner.$dirty && mctFormInner.$valid,
invalid: mctFormInner.$dirty && !mctFormInner.$valid,
- first: $index < 1
+ first: $index < 1,
+ 'l-controls-first': row.layout === 'control-first',
+ 'l-controls-under': row.layout === 'controls-under'
}">
{{row.name}}
diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js
index 783f61e1f2..b0c0518d24 100644
--- a/platform/representation/src/MCTRepresentation.js
+++ b/platform/representation/src/MCTRepresentation.js
@@ -137,15 +137,13 @@ define(
}
function unchanged(canRepresent, canEdit, idPath, key) {
- return canRepresent &&
- couldRepresent &&
- key === lastKey &&
- idPath.length === lastIdPath.length &&
+ return (canRepresent === couldRepresent) &&
+ (key === lastKey) &&
+ (idPath.length === lastIdPath.length) &&
idPath.every(function (id, i) {
return id === lastIdPath[i];
}) &&
- canEdit &&
- couldEdit;
+ (canEdit === couldEdit);
}
function getIdPath(domainObject) {
diff --git a/platform/representation/src/gestures/DropGesture.js b/platform/representation/src/gestures/DropGesture.js
index 6af580d9c3..76df23b043 100644
--- a/platform/representation/src/gestures/DropGesture.js
+++ b/platform/representation/src/gestures/DropGesture.js
@@ -114,7 +114,8 @@ define(
// destination domain object's composition, and persist
// the change.
if (id) {
- $q.when(action && action.perform()).then(function () {
+ e.preventDefault();
+ $q.when(action && action.perform()).then(function (result) {
//Don't go into edit mode for folders
if (domainObjectType!=='folder') {
editableDomainObject.getCapability('action').perform('edit');
diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js
index 05c99f24bc..7608070d3e 100644
--- a/platform/representation/test/MCTRepresentationSpec.js
+++ b/platform/representation/test/MCTRepresentationSpec.js
@@ -246,53 +246,70 @@ define(
expect(mockScope.testCapability).toBeUndefined();
});
- it("detects changes among linked instances", function () {
- var mockContext = jasmine.createSpyObj('context', ['getPath']),
- mockContext2 = jasmine.createSpyObj('context', ['getPath']),
+ describe("when a domain object has been observed", function () {
+ var mockContext,
+ mockContext2,
+ mockLink,
+ mockParent;
+
+ beforeEach(function () {
+ mockContext = jasmine.createSpyObj('context', ['getPath']);
+ mockContext2 = jasmine.createSpyObj('context', ['getPath']);
mockLink = jasmine.createSpyObj(
'linkedObject',
DOMAIN_OBJECT_METHODS
- ),
+ );
mockParent = jasmine.createSpyObj(
'parentObject',
DOMAIN_OBJECT_METHODS
- ),
- callCount;
+ );
- mockDomainObject.getCapability.andCallFake(function (c) {
- return c === 'context' && mockContext;
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return c === 'context' && mockContext;
+ });
+ mockLink.getCapability.andCallFake(function (c) {
+ return c === 'context' && mockContext2;
+ });
+ mockDomainObject.hasCapability.andCallFake(function (c) {
+ return c === 'context';
+ });
+ mockLink.hasCapability.andCallFake(function (c) {
+ return c === 'context';
+ });
+ mockLink.getModel.andReturn({});
+
+ mockContext.getPath.andReturn([mockDomainObject]);
+ mockContext2.getPath.andReturn([mockParent, mockLink]);
+
+ mockLink.getId.andReturn('test-id');
+ mockDomainObject.getId.andReturn('test-id');
+
+ mockParent.getId.andReturn('parent-id');
+
+ mockScope.key = "abc";
+ mockScope.domainObject = mockDomainObject;
+
+ mockScope.$watch.calls[0].args[1]();
});
- mockLink.getCapability.andCallFake(function (c) {
- return c === 'context' && mockContext2;
+
+ it("detects subsequent changes among linked instances", function () {
+ var callCount = mockChangeTemplate.calls.length;
+
+ mockScope.domainObject = mockLink;
+ mockScope.$watch.calls[0].args[1]();
+
+ expect(mockChangeTemplate.calls.length)
+ .toEqual(callCount + 1);
});
- mockDomainObject.hasCapability.andCallFake(function (c) {
- return c === 'context';
+
+ it("does not trigger excess template changes for same instances", function () {
+ var callCount = mockChangeTemplate.calls.length;
+ mockScope.$watch.calls[0].args[1]();
+ expect(mockChangeTemplate.calls.length).toEqual(callCount);
});
- mockLink.hasCapability.andCallFake(function (c) {
- return c === 'context';
- });
- mockLink.getModel.andReturn({});
-
- mockContext.getPath.andReturn([mockDomainObject]);
- mockContext2.getPath.andReturn([mockParent, mockLink]);
-
- mockLink.getId.andReturn('test-id');
- mockDomainObject.getId.andReturn('test-id');
-
- mockParent.getId.andReturn('parent-id');
-
- mockScope.key = "abc";
- mockScope.domainObject = mockDomainObject;
-
- mockScope.$watch.calls[0].args[1]();
- callCount = mockChangeTemplate.calls.length;
-
- mockScope.domainObject = mockLink;
- mockScope.$watch.calls[0].args[1]();
-
- expect(mockChangeTemplate.calls.length)
- .toEqual(callCount + 1);
});
+
+
});
}
);
diff --git a/platform/representation/test/gestures/DropGestureSpec.js b/platform/representation/test/gestures/DropGestureSpec.js
index ae8118d909..b3f043cfa9 100644
--- a/platform/representation/test/gestures/DropGestureSpec.js
+++ b/platform/representation/test/gestures/DropGestureSpec.js
@@ -192,6 +192,11 @@ define(
);
});
+ it("invokes preventDefault on drop", function () {
+ callbacks.drop(mockEvent);
+ expect(mockEvent.preventDefault).toHaveBeenCalled();
+ });
+
});
}
-);
\ No newline at end of file
+);
diff --git a/platform/search/src/services/GenericSearchProvider.js b/platform/search/src/services/GenericSearchProvider.js
index caa5dec428..ce2ae642ed 100644
--- a/platform/search/src/services/GenericSearchProvider.js
+++ b/platform/search/src/services/GenericSearchProvider.js
@@ -120,9 +120,13 @@ define([
provider = this;
mutationTopic.listen(function (mutatedObject) {
- var id = mutatedObject.getId();
- provider.indexedIds[id] = false;
- provider.scheduleForIndexing(id);
+ var status = mutatedObject.getCapability('status');
+ if (!status || !status.get('editing')) {
+ provider.index(
+ mutatedObject.getId(),
+ mutatedObject.getModel()
+ );
+ }
});
};
diff --git a/platform/search/test/services/GenericSearchProviderSpec.js b/platform/search/test/services/GenericSearchProviderSpec.js
index ecc76f044e..c9fc1ff03d 100644
--- a/platform/search/test/services/GenericSearchProviderSpec.js
+++ b/platform/search/test/services/GenericSearchProviderSpec.js
@@ -103,12 +103,19 @@ define([
.toHaveBeenCalledWith(jasmine.any(Function));
});
- it('reschedules indexing when mutation occurs', function () {
+ it('re-indexes when mutation occurs', function () {
var mockDomainObject =
- jasmine.createSpyObj('domainObj', ['getId']);
+ jasmine.createSpyObj('domainObj', [
+ 'getId',
+ 'getModel',
+ 'getCapability'
+ ]),
+ testModel = { some: 'model' };
mockDomainObject.getId.andReturn("some-id");
+ mockDomainObject.getModel.andReturn(testModel);
+ spyOn(provider, 'index').andCallThrough();
mutationTopic.listen.mostRecentCall.args[0](mockDomainObject);
- expect(provider.scheduleForIndexing).toHaveBeenCalledWith('some-id');
+ expect(provider.index).toHaveBeenCalledWith('some-id', testModel);
});
it('starts indexing roots', function () {
diff --git a/platform/telemetry/src/TelemetryFormatter.js b/platform/telemetry/src/TelemetryFormatter.js
index 92807f5cb8..b0dd94b3db 100644
--- a/platform/telemetry/src/TelemetryFormatter.js
+++ b/platform/telemetry/src/TelemetryFormatter.js
@@ -24,10 +24,6 @@ define(
[],
function () {
- // Date format to use for domain values; in particular,
- // use day-of-year instead of month/day
- var VALUE_FORMAT_DIGITS = 3;
-
/**
* The TelemetryFormatter is responsible for formatting (as text
* for display) values along either the domain (usually time) or
@@ -70,8 +66,8 @@ define(
* @returns {string} a textual representation of the
* value, suitable for display.
*/
- TelemetryFormatter.prototype.formatRangeValue = function (v) {
- return isNaN(v) ? String(v) : v.toFixed(VALUE_FORMAT_DIGITS);
+ TelemetryFormatter.prototype.formatRangeValue = function (v, key) {
+ return String(v);
};
return TelemetryFormatter;
diff --git a/platform/telemetry/test/TelemetryFormatterSpec.js b/platform/telemetry/test/TelemetryFormatterSpec.js
index d8e8dacbf0..cf3794d19b 100644
--- a/platform/telemetry/test/TelemetryFormatterSpec.js
+++ b/platform/telemetry/test/TelemetryFormatterSpec.js
@@ -57,7 +57,10 @@ define(
});
it("formats ranges as values", function () {
- expect(formatter.formatRangeValue(10)).toEqual("10.000");
+ var value = 3.14159265352979323846264338, // not pi
+ formatted = formatter.formatRangeValue(value);
+ // Make sure we don't lose information by formatting
+ expect(parseFloat(formatted)).toEqual(value);
});
});
}
diff --git a/test-main.js b/test-main.js
index caa306c1b6..90da6dabb9 100644
--- a/test-main.js
+++ b/test-main.js
@@ -53,7 +53,8 @@ requirejs.config({
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
- "uuid": "bower_components/node-uuid/uuid"
+ "uuid": "bower_components/node-uuid/uuid",
+ "zepto": "bower_components/zepto/zepto.min"
},
"shim": {
@@ -68,6 +69,9 @@ requirejs.config({
},
"screenfull": {
"exports": "screenfull"
+ },
+ "zepto": {
+ "exports": "Zepto"
}
},