diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 93cb95bb7a..7016462a5a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -6,12 +6,13 @@ Victor Woeltjen September 23, 2015 Document Version 1.1 -Date | Version | Summary of Changes | Author -------------------- | --------- | ----------------------- | --------------- -April 29, 2015 | 0 | Initial Draft | Victor Woeltjen -May 12, 2015 | 0.1 | | Victor Woeltjen -June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen -October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +Date | Version | Summary of Changes | Author +------------------- | --------- | ------------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry # Introduction The purpose of this guide is to familiarize software developers with the Open @@ -1600,6 +1601,61 @@ there are items . ] } +## Table + +The `mct-table` directive provides a generic table component, with optional +sorting and filtering capabilities. The table can be pre-populated with data +by setting the `rows` parameter, and it can be updated in real-time using the +`add:row` and `remove:row` broadcast events. The table will expand to occupy +100% of the size of its containing element. The table is highly optimized for +very large data sets. + +### Events + +The table supports two events for notifying that the rows have changed. For +performance reasons, the table does not monitor the content of `rows` +constantly. + +* `add:row`: A `$broadcast` event that will notify the table that a new row +has been added to the table. + +eg. The code below adds a new row, and alerts the table using the `add:row` +event. Sorting and filtering will be applied automatically by the table component. + +``` +$scope.rows.push(newRow); +$scope.$broadcast('add:row', $scope.rows.length-1); +``` + +* `remove:row`: A `$broadcast` event that will notify the table that a row +should be removed from the table. + +eg. The code below removes a row from the rows array, and then alerts the table +to its removal. + +``` +$scope.rows.slice(5, 1); +$scope.$broadcast('remove:row', 5); +``` + +### Parameters + +* `headers`: An array of string values which will constitute the column titles + that appear at the top of the table. Corresponding values are specified in + the rows using the header title provided here. +* `rows`: An array of objects containing row values. Each element in the +array must be an associative array, where the key corresponds to a column header. +* `enableFilter`: A boolean that if true, will enable searching and result +filtering. When enabled, each column will have a text input field that can be +used to filter the table rows in real time. +* `enableSort`: A boolean determining whether rows can be sorted. If true, +sorting will be enabled allowing sorting by clicking on column headers. Only +one column may be sorted at a time. +* `autoScroll`: A boolean value that if true, will cause the table to automatically +scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually +by scrolling away from the bottom of the table, and can also be enabled manually +by scrolling to the bottom of the table rows. + # Services The Open MCT Web platform provides a variety of services which can be retrieved diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 77ead67c04..0220b7dc6d 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -23,16 +23,16 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RTTelemetryTableController", - "./src/controllers/TelemetryTableController", + "./src/controllers/RealtimeTableController", + "./src/controllers/HistoricalTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "legacyRegistry" ], function ( MCTTable, - RTTelemetryTableController, - TelemetryTableController, + RealtimeTableController, + HistoricalTableController, TableOptionsController, Region, InspectorRegion, @@ -109,13 +109,13 @@ define([ ], "controllers": [ { - "key": "TelemetryTableController", - "implementation": TelemetryTableController, + "key": "HistoricalTableController", + "implementation": HistoricalTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { - "key": "RTTelemetryTableController", - "implementation": RTTelemetryTableController, + "key": "RealtimeTableController", + "implementation": RealtimeTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { @@ -130,7 +130,7 @@ define([ "name": "Historical Table", "key": "table", "glyph": "\ue604", - "templateUrl": "templates/table.html", + "templateUrl": "templates/historical-table.html", "needs": [ "telemetry" ], @@ -161,6 +161,12 @@ define([ "key": "table-options-edit", "templateUrl": "templates/table-options-edit.html" } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/table.css", + "priority": "mandatory" + } ] } }); diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss new file mode 100644 index 0000000000..a79cfac4c6 --- /dev/null +++ b/platform/features/table/res/sass/table.scss @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +.sizing-table { + min-width: 100%; + z-index: -1; + visibility: hidden; + position: absolute; + + //Add some padding to allow for decorations such as limits indicator + td { + padding-right: 15px; + padding-left: 10px; + white-space: nowrap; + } +} +.mct-table { + table-layout: fixed; + th { + box-sizing: border-box; + } + tbody { + tr { + position: absolute; + } + td { + white-space: nowrap; + overflow: hidden; + box-sizing: border-box; + } + } +} \ No newline at end of file diff --git a/platform/features/table/res/templates/table.html b/platform/features/table/res/templates/historical-table.html similarity index 71% rename from platform/features/table/res/templates/table.html rename to platform/features/table/res/templates/historical-table.html index c63f6d63fc..9917c41dcc 100644 --- a/platform/features/table/res/templates/table.html +++ b/platform/features/table/res/templates/historical-table.html @@ -1,4 +1,4 @@ -
{{header}} | +
+ {{sizingRow[header].text}} + |
@@ -41,21 +42,15 @@ | |
---|---|
{{ visibleRow.contents[header].text }}
diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html
index 326c5b847b..d35015c96c 100644
--- a/platform/features/table/res/templates/rt-table.html
+++ b/platform/features/table/res/templates/rt-table.html
@@ -1,4 +1,4 @@
-
+
+ * The code above adds a new row, and alerts the table using the
+ * add:row event. Sorting and filtering will be applied
+ * automatically by the table component.
+ *
+ * - 'remove:row': A $broadcast event that will notify the table that a
+ * row should be removed from the table.
+ * eg.
+ *
+ * The code above removes a row from the rows array, and then alerts
+ * the table to its removal.
+ *
+ * @memberof platform/features/table
+ * @param {string[]} headers The column titles to appear at the top
+ * of the table. Corresponding values are specified in the rows
+ * using the header title provided here.
+ * @param {Object[]} rows The row content. Each row is an object
+ * with key-value pairs where the key corresponds to a header
+ * specified in the headers parameter.
+ * @param {boolean} enableFilter If true, values will be searchable
+ * and results filtered
+ * @param {boolean} enableSort If true, sorting will be enabled
+ * allowing sorting by clicking on column headers
+ * @param {boolean} autoScroll If true, table will automatically
+ * scroll to the bottom as new data arrives. Auto-scroll can be
+ * disengaged manually by scrolling away from the bottom of the
+ * table, and can also be enabled manually by scrolling to the bottom of
+ * the table rows.
+ *
* @constructor
*/
function MCTTable($timeout) {
diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js
index 86a18aee5a..bfc6f50edb 100644
--- a/platform/features/table/test/TableConfigurationSpec.js
+++ b/platform/features/table/test/TableConfigurationSpec.js
@@ -116,10 +116,10 @@ define(
}];
beforeEach(function() {
- table.buildColumns(metadata);
+ table.populateColumns(metadata);
});
- it("populates the columns attribute", function() {
+ it("populates columns", function() {
expect(table.columns.length).toBe(5);
});
@@ -141,7 +141,7 @@ define(
it("Provides a default configuration with all columns" +
" visible", function() {
- var configuration = table.getColumnConfiguration();
+ var configuration = table.buildColumnConfiguration();
expect(configuration).toBeDefined();
expect(Object.keys(configuration).every(function(key){
@@ -160,7 +160,7 @@ define(
};
mockModel.configuration = modelConfig;
- tableConfig = table.getColumnConfiguration();
+ tableConfig = table.buildColumnConfiguration();
expect(tableConfig).toBeDefined();
expect(tableConfig['Range 1']).toBe(false);
diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js
similarity index 94%
rename from platform/features/table/test/controllers/TelemetryTableControllerSpec.js
rename to platform/features/table/test/controllers/HistoricalTableControllerSpec.js
index 03f62f11e3..f000529467 100644
--- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
+++ b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js
@@ -23,7 +23,7 @@
define(
[
- "../../src/controllers/TelemetryTableController"
+ "../../src/controllers/HistoricalTableController"
],
function (TableController) {
"use strict";
@@ -73,14 +73,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
- 'buildColumns',
- 'getColumnConfiguration',
+ 'populateColumns',
+ 'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
- mockTable.getColumnConfiguration.andReturn(mockConfiguration);
+ mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockDomainObject= jasmine.createSpyObj('domainObject', [
'getCapability',
@@ -126,21 +126,18 @@ define(
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
});
- describe('the controller makes use of the table', function () {
+ describe('makes use of the table', function () {
it('to create column definitions from telemetry' +
' metadata', function () {
controller.setup();
- expect(mockTable.buildColumns).toHaveBeenCalled();
+ expect(mockTable.populateColumns).toHaveBeenCalled();
});
it('to create column configuration, which is written to the' +
' object model', function () {
- var mockModel = {};
-
controller.setup();
- expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
- expect(mockTable.saveColumnConfiguration).toHaveBeenCalled();
+ expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
});
});
diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js
index 5e38c7e651..175d8fed0a 100644
--- a/platform/features/table/test/controllers/MCTTableControllerSpec.js
+++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js
@@ -58,15 +58,18 @@ define(
mockElement = jasmine.createSpyObj('element', [
'find',
+ 'prop',
'on'
]);
mockElement.find.andReturn(mockElement);
+ mockElement.prop.andReturn(0);
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
+ spyOn(controller, 'setVisibleRows');
});
it('Reacts to changes to filters, headers, and rows', function() {
@@ -115,7 +118,7 @@ define(
});
it('Sets rows on scope when rows change', function() {
- controller.updateRows(testRows);
+ controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
expect(mockScope.displayRows).toEqual(testRows);
});
@@ -127,7 +130,7 @@ define(
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
};
- controller.updateRows(testRows);
+ controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
addRowFunc(undefined, 3);
@@ -136,10 +139,8 @@ define(
it('Supports removing rows individually', function() {
var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1];
- controller.updateRows(testRows);
+ controller.setRows(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();
@@ -179,7 +180,54 @@ define(
expect(sortedRows[2].col2.text).toEqual('abc');
});
- describe('Adding new rows', function() {
+ it('correctly sorts rows of differing types', function () {
+ mockScope.sortColumn = 'col2';
+ mockScope.sortDirection = 'desc';
+
+ testRows.push({
+ 'col1': {'text': 'row4 col1'},
+ 'col2': {'text': '123'},
+ 'col3': {'text': 'row4 col3'}
+ });
+ testRows.push({
+ 'col1': {'text': 'row5 col1'},
+ 'col2': {'text': '456'},
+ 'col3': {'text': 'row5 col3'}
+ });
+ testRows.push({
+ 'col1': {'text': 'row5 col1'},
+ 'col2': {'text': ''},
+ 'col3': {'text': 'row5 col3'}
+ });
+
+ sortedRows = controller.sortRows(testRows);
+ expect(sortedRows[0].col2.text).toEqual('ghi');
+ expect(sortedRows[1].col2.text).toEqual('def');
+ expect(sortedRows[2].col2.text).toEqual('abc');
+
+ expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456');
+ expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123');
+ expect(sortedRows[sortedRows.length-1].col2.text).toEqual('');
+ });
+
+ describe('The sort comparator', function () {
+ it('Correctly sorts different data types', function () {
+ var val1 = "",
+ val2 = "1",
+ val3 = "2016-04-05 18:41:30.713Z",
+ val4 = "1.1",
+ val5 = "8.945520958175627e-13";
+ mockScope.sortDirection = "asc";
+
+ expect(controller.sortComparator(val1, val2)).toEqual(-1);
+ expect(controller.sortComparator(val3, val1)).toEqual(1);
+ expect(controller.sortComparator(val3, val2)).toEqual(1);
+ expect(controller.sortComparator(val4, val2)).toEqual(1);
+ expect(controller.sortComparator(val2, val5)).toEqual(1);
+ });
+ });
+
+ describe('Adding new rows', function () {
var row4,
row5,
row6;
@@ -210,20 +258,20 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.rows.push(row4);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
mockScope.rows.push(row5);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(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);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
@@ -239,18 +287,18 @@ define(
mockScope.displayRows = controller.filterRows(testRows);
mockScope.rows.push(row5);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(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);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
it('Adds new rows at the correct sort position when' +
- ' not sorted ', function() {
+ ' not sorted ', function () {
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
@@ -258,14 +306,33 @@ define(
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row5);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
- controller.newRow(undefined, mockScope.rows.length-1);
+ controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
+ it('Resizes columns if length of any columns in new' +
+ ' row exceeds corresponding existing column', function() {
+ var row7 = {
+ 'col1': {'text': 'row6 col1'},
+ 'col2': {'text': 'some longer string'},
+ 'col3': {'text': 'row6 col3'}
+ };
+
+ mockScope.sortColumn = undefined;
+ mockScope.sortDirection = undefined;
+ mockScope.filters = {};
+
+ mockScope.displayRows = testRows.slice(0);
+
+ mockScope.rows.push(row7);
+ controller.addRow(undefined, mockScope.rows.length-1);
+ expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
+ });
+
});
});
diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js
similarity index 94%
rename from platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js
rename to platform/features/table/test/controllers/RealtimeTableControllerSpec.js
index 59911d1771..e0b6978d88 100644
--- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js
+++ b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js
@@ -23,7 +23,7 @@
define(
[
- "../../src/controllers/RTTelemetryTableController"
+ "../../src/controllers/RealtimeTableController"
],
function (TableController) {
"use strict";
@@ -77,14 +77,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
- 'buildColumns',
- 'getColumnConfiguration',
+ 'populateColumns',
+ 'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
- mockTable.getColumnConfiguration.andReturn(mockConfiguration);
+ mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockTable.getRowValues.andReturn(mockTableRow);
mockDomainObject= jasmine.createSpyObj('domainObject', [
@@ -107,13 +107,16 @@ define(
'unsubscribe',
'getDatum',
'promiseTelemetryObjects',
- 'getTelemetryObjects'
+ 'getTelemetryObjects',
+ 'request'
]);
+
// Arbitrary array with non-zero length, contents are not
// used by mocks
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
mockTelemetryHandle.getDatum.andReturn({});
+ mockTelemetryHandle.request.andReturn(promise(undefined));
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'
diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js
index 9de96b5f52..fd6d8b43fe 100644
--- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js
+++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js
@@ -47,18 +47,36 @@ define(
'listen'
]);
mockDomainObject = jasmine.createSpyObj('domainObject', [
- 'getCapability'
+ 'getCapability',
+ 'getModel'
]);
mockDomainObject.getCapability.andReturn(mockCapability);
+ mockDomainObject.getModel.andReturn({});
+
mockScope = jasmine.createSpyObj('scope', [
- '$watchCollection'
+ '$watchCollection',
+ '$watch',
+ '$on'
]);
mockScope.domainObject = mockDomainObject;
controller = new TableOptionsController(mockScope);
});
+ it('Listens for changing domain object', function() {
+ expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
+ });
+
+ it('On destruction of controller, destroys listeners', function() {
+ var unlistenFunc = jasmine.createSpy("unlisten");
+ controller.listeners.push(unlistenFunc);
+ expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
+ mockScope.$on.mostRecentCall.args[1]();
+ expect(unlistenFunc).toHaveBeenCalled();
+ });
+
it('Registers a listener for mutation events on the object', function() {
+ mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockCapability.listen).toHaveBeenCalled();
});
|