diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 6353a4505b..486f9ab9a6 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -39,7 +39,6 @@ define( function TableConfiguration(domainObject, telemetryFormatter) { this.domainObject = domainObject; this.columns = []; - this.columnConfiguration = {}; this.telemetryFormatter = telemetryFormatter; } @@ -145,30 +144,16 @@ define( {}).columns || {}; }; - function configEqual(obj1, obj2) { - var obj1Keys = Object.keys(obj1), - obj2Keys = Object.keys(obj2); - return (obj1Keys.length === obj2Keys.length) && - obj1Keys.every(function (key) { - return obj1[key] === obj2[key]; - }); - } - /** - * Set the established configuration on the domain object. Will noop - * if configuration is unchanged + * Set the established configuration on the domain object * @private */ TableConfiguration.prototype.saveColumnConfiguration = function (columnConfig) { - var self = this; - if (!configEqual(this.columnConfiguration, columnConfig)) { - this.domainObject.useCapability('mutation', function (model) { - model.configuration = model.configuration || {}; - model.configuration.table = model.configuration.table || {}; - model.configuration.table.columns = columnConfig; - self.columnConfiguration = columnConfig; - }); - } + this.domainObject.useCapability('mutation', function (model) { + model.configuration = model.configuration || {}; + model.configuration.table = model.configuration.table || {}; + model.configuration.table.columns = columnConfig; + }); }; /** diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 2204861a0c..ad13647766 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -67,8 +67,8 @@ define( $scope.$watchCollection('filters', function () { self.updateRows($scope.rows); }); - $scope.$watch('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) @@ -101,19 +101,13 @@ define( */ MCTTableController.prototype.newRow = function (event, rowIndex) { var row = this.$scope.rows[rowIndex]; - //If rows.length === 1 we need to calculate column widths etc. - // so do the updateRows logic, rather than the 'add row' logic - if (this.$scope.rows.length === 1){ - this.updateRows(this.$scope.rows); - } else { - //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)); + //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)); }; /** diff --git a/platform/features/table/src/controllers/RTTelemetryTableController.js b/platform/features/table/src/controllers/RTTelemetryTableController.js index 2ed3e005cd..8a61d61b5e 100644 --- a/platform/features/table/src/controllers/RTTelemetryTableController.js +++ b/platform/features/table/src/controllers/RTTelemetryTableController.js @@ -69,36 +69,53 @@ define( 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.addHistoricalData = function () { - //Noop for realtime table - }; + RTTelemetryTableController.prototype.subscribe = function () { + var self = this; + self.$scope.rows = undefined; + (this.subscriptions || []).forEach(function (unsubscribe){ + unsubscribe(); + }); - /** - * Handling for real-time data - */ - RTTelemetryTableController.prototype.updateRealtime = function () { - var datum, - row, - self = this; if (this.handle) { - this.handle.getTelemetryObjects().forEach(function (telemetryObject) { + 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); - self.$scope.rows.push(row); + 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(); + 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); } - - 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/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 3c3fb7665d..e579c5eeb8 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -58,14 +58,14 @@ define( telemetryFormatter); this.changeListeners = []; - $scope.rows = []; + $scope.rows = undefined; // Subscribe to telemetry when a domain object becomes available this.$scope.$watch('domainObject', function(domainObject){ if (!domainObject) return; - self.subscribe(domainObject); + self.subscribe(); self.registerChangeListeners(); }); @@ -79,28 +79,14 @@ define( * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - var self = this; - this.changeListeners.forEach(function (listener) { return listener && listener(); }); this.changeListeners = []; - - /** - * Listen to all children for model mutation events that might - * indicate that metadata is available, or that composition has - * changed. - */ - if (this.$scope.domainObject.hasCapability('composition')) { - this.$scope.domainObject.useCapability('composition').then(function (composees) { - composees.forEach(function (composee) { - self.changeListeners.push(composee.getCapability('mutation').listen(self.setup.bind(self))); - }); - }); - } - - //Register mutation listener for the parent itself - self.changeListeners.push(self.$scope.domainObject.getCapability('mutation').listen(this.setup.bind(this))); + // When composition changes, re-subscribe to the various + // telemetry subscriptions + 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', @@ -124,38 +110,26 @@ define( */ TelemetryTableController.prototype.subscribe = function () { var self = this; - this.$scope.rows = []; if (this.handle) { this.handle.unsubscribe(); } //Noop because not supporting realtime data right now - function update(){ - //Is there anything to show? - if (self.table.columns.length > 0){ - self.updateRealtime(); - } + function noop(){ } this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - self.$scope.domainObject, - update, + this.$scope.domainObject, + noop, true // Lossless ); + this.handle.request({}).then(this.addHistoricalData.bind(this)); - //Call setup at least once this.setup(); }; - /** - * Override this method to define handling for realtime data. - */ - TelemetryTableController.prototype.updateRealtime = function () { - //Noop in an historical table - }; - /** * Populates historical data on scope when it becomes available * @private @@ -183,52 +157,45 @@ define( */ TelemetryTableController.prototype.setup = function () { var handle = this.handle, - domainObject = this.$scope.domainObject, table = this.table, - self = this, - metadatas = []; + self = this; - function addMetadata(object) { - if (object.hasCapability('telemetry') && - object.getCapability('telemetry').getMetadata()){ - metadatas.push(object.getCapability('telemetry').getMetadata()); - } - } + if (handle) { + handle.promiseTelemetryObjects().then(function () { + table.buildColumns(handle.getMetadata()); - function buildAndFilterColumns(){ - if (metadatas && metadatas.length > 0){ - self.$scope.rows = []; - table.buildColumns(metadatas); self.filterColumns(); - } - } - //Add telemetry metadata (if any) for parent object - addMetadata(domainObject); - - //If object is composed of multiple objects, also add - // telemetry metadata from children - if (domainObject.hasCapability('composition')) { - domainObject.useCapability('composition').then(function (composition) { - composition.forEach(addMetadata); - }).then(function () { - //Build columns based on available metadata - buildAndFilterColumns(); + // When table column configuration changes, (due to being + // selected or deselected), filter columns appropriately. + self.changeListeners.push(self.$scope.$watchCollection( + 'domainObject.getModel().configuration.table.columns', + self.filterColumns.bind(self) + )); }); - } else { - //Build columns based on collected metadata - buildAndFilterColumns(); } }; + /** + * @private + * @param object The object for which data is available (table may + * be composed of multiple objects) + * @param datum The data received from the telemetry source + */ + TelemetryTableController.prototype.updateRows = function (object, datum) { + this.$scope.rows.push(this.table.getRowValues(object, datum)); + }; + /** * When column configuration changes, update the visible headers * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function () { - var columnConfig = this.table.getColumnConfiguration(); - this.table.saveColumnConfiguration(columnConfig); + TelemetryTableController.prototype.filterColumns = function (columnConfig) { + if (!columnConfig){ + columnConfig = this.table.getColumnConfiguration(); + this.table.saveColumnConfiguration(columnConfig); + } //Populate headers with visible columns (determined by configuration) this.$scope.headers = Object.keys(columnConfig).filter(function (column) { return columnConfig[column]; diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js index 5fbab46cf0..59911d1771 100644 --- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js @@ -89,24 +89,20 @@ define( mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', - 'hasCapability', 'useCapability', 'getModel' ]); mockDomainObject.getModel.andReturn({}); - mockDomainObject.hasCapability.andReturn(true); mockDomainObject.getCapability.andReturn( { getMetadata: function (){ return {ranges: [{format: 'string'}]}; } }); - mockDomainObject.useCapability.andReturn(promise([])); mockScope.domainObject = mockDomainObject; mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'request', 'getMetadata', 'unsubscribe', 'getDatum', @@ -117,7 +113,6 @@ define( // used by mocks mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandle.getDatum.andReturn({}); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ @@ -128,8 +123,6 @@ define( controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter); controller.table = mockTable; controller.handle = mockTelemetryHandle; - spyOn(controller, 'updateRealtime'); - controller.updateRealtime.andCallThrough(); }); it('registers for streaming telemetry', function () { @@ -137,16 +130,14 @@ define( expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true); }); - describe('when receiving new telemetry', function () { + describe('receives new telemetry', function () { beforeEach(function() { controller.subscribe(); mockScope.rows = []; - mockTable.columns = ['a', 'b']; }); it('updates table with new streaming telemetry', function () { mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(controller.updateRealtime).toHaveBeenCalled(); expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0); }); it('observes the row limit', function () { diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js index a872fa63e9..03f62f11e3 100644 --- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -33,13 +33,7 @@ define( mockTelemetryHandler, mockTelemetryHandle, mockTelemetryFormatter, - mockTelemetryCapability, mockDomainObject, - mockChild, - mockMutationCapability, - mockCompositionCapability, - childMutationCapability, - capabilities = {}, mockTable, mockConfiguration, watches, @@ -70,17 +64,6 @@ define( mockScope.$watchCollection.andCallFake(function (expression, callback){ watches[expression] = callback; }); - mockTelemetryCapability = jasmine.createSpyObj('telemetryCapability', - ['getMetadata'] - ); - mockTelemetryCapability.getMetadata.andReturn({}); - capabilities.telemetry = mockTelemetryCapability; - - mockCompositionCapability = jasmine.createSpyObj('compositionCapability', - ['invoke'] - ); - mockCompositionCapability.invoke.andReturn(promise([])); - capabilities.composition = mockCompositionCapability; mockConfiguration = { 'range1': true, @@ -98,36 +81,13 @@ define( ); mockTable.columns = []; mockTable.getColumnConfiguration.andReturn(mockConfiguration); - mockMutationCapability = jasmine.createSpyObj('mutationCapability', [ - "listen" - ]); - capabilities.mutation = mockMutationCapability; - mockDomainObject = jasmine.createSpyObj('domainObject', [ + mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', - 'hasCapability', 'useCapability', 'getModel' ]); - mockChild = jasmine.createSpyObj('domainObject', [ - 'getCapability' - ]); - childMutationCapability = jasmine.createSpyObj('childMutationCapability', [ - "listen" - ]); - mockChild.getCapability.andReturn(childMutationCapability); - - mockDomainObject.getModel.andReturn({}); - mockDomainObject.hasCapability.andCallFake(function (name){ - return typeof capabilities[name] !== 'undefined'; - }); - mockDomainObject.useCapability.andCallFake(function (capability){ - return capabilities[capability] && capabilities[capability].invoke && capabilities[capability].invoke(); - }); - mockDomainObject.getCapability.andCallFake(function (name){ - return capabilities[name]; - }); mockScope.domainObject = mockDomainObject; @@ -143,7 +103,6 @@ define( mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandle.getTelemetryObjects.andReturn([]); - mockTelemetryHandle.getMetadata.andReturn(["a", "b", "c"]); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ 'handle' @@ -168,6 +127,7 @@ define( }); describe('the controller makes use of the table', function () { + it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); @@ -239,6 +199,14 @@ define( expect(controller.subscribe).toHaveBeenCalled(); }); + it('triggers telemetry subscription update when domain' + + ' object composition changes', function () { + controller.registerChangeListeners(); + expect(watches['domainObject.getModel().composition']).toBeDefined(); + watches['domainObject.getModel().composition'](); + expect(controller.subscribe).toHaveBeenCalled(); + }); + it('triggers telemetry subscription update when time' + ' conductor bounds change', function () { controller.registerChangeListeners(); @@ -246,21 +214,12 @@ define( watches['telemetry:display:bounds'](); expect(controller.subscribe).toHaveBeenCalled(); }); - it('Listens for changes to object model', function () { - controller.registerChangeListeners(); - expect(mockMutationCapability.listen).toHaveBeenCalled(); - }); - it('Listens for changes to child model', function () { - mockCompositionCapability.invoke.andReturn(promise([mockChild])); - controller.registerChangeListeners(); - expect(childMutationCapability.listen).toHaveBeenCalled(); - }); - - it('Recalculates columns when model changes occur', function () { - controller.registerChangeListeners(); - expect(mockMutationCapability.listen).toHaveBeenCalled(); - mockMutationCapability.listen.mostRecentCall.args[0](); + it('triggers refiltering of the columns when configuration' + + ' changes', function () { + controller.setup(); + expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined(); + watches['domainObject.getModel().configuration.table.columns'](); expect(controller.filterColumns).toHaveBeenCalled(); });