diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 486f9ab9a6..6353a4505b 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -39,6 +39,7 @@ define( function TableConfiguration(domainObject, telemetryFormatter) { this.domainObject = domainObject; this.columns = []; + this.columnConfiguration = {}; this.telemetryFormatter = telemetryFormatter; } @@ -144,16 +145,30 @@ 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 + * Set the established configuration on the domain object. Will noop + * if configuration is unchanged * @private */ TableConfiguration.prototype.saveColumnConfiguration = function (columnConfig) { - this.domainObject.useCapability('mutation', function (model) { - model.configuration = model.configuration || {}; - model.configuration.table = model.configuration.table || {}; - model.configuration.table.columns = 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; + }); + } }; /** diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index ad13647766..2204861a0c 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('headers', this.updateHeaders.bind(this)); $scope.$watch('rows', this.updateRows.bind(this)); + $scope.$watch('headers', this.updateHeaders.bind(this)); /* * Listen for rows added individually (eg. for real-time tables) @@ -101,13 +101,19 @@ define( */ 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); - } + //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)); + 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 8a61d61b5e..2ed3e005cd 100644 --- a/platform/features/table/src/controllers/RTTelemetryTableController.js +++ b/platform/features/table/src/controllers/RTTelemetryTableController.js @@ -69,53 +69,36 @@ 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.subscribe = function () { - var self = this; - self.$scope.rows = undefined; - (this.subscriptions || []).forEach(function (unsubscribe){ - unsubscribe(); - }); + RTTelemetryTableController.prototype.addHistoricalData = function () { + //Noop for realtime table + }; + /** + * Handling for real-time data + */ + RTTelemetryTableController.prototype.updateRealtime = function () { + var datum, + row, + self = this; if (this.handle) { - this.handle.unsubscribe(); - } - - function updateData(){ - var datum, - row; - self.handle.getTelemetryObjects().forEach(function (telemetryObject){ + this.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); + 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); + 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/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index e579c5eeb8..3c3fb7665d 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 = undefined; + $scope.rows = []; // Subscribe to telemetry when a domain object becomes available this.$scope.$watch('domainObject', function(domainObject){ if (!domainObject) return; - self.subscribe(); + self.subscribe(domainObject); self.registerChangeListeners(); }); @@ -79,14 +79,28 @@ define( * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { + var self = this; + 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))); + + /** + * 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))); //Change of bounds in time conductor this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', @@ -110,26 +124,38 @@ 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 noop(){ + function update(){ + //Is there anything to show? + if (self.table.columns.length > 0){ + self.updateRealtime(); + } } this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - noop, + self.$scope.domainObject, + update, 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 @@ -157,33 +183,42 @@ define( */ TelemetryTableController.prototype.setup = function () { var handle = this.handle, + domainObject = this.$scope.domainObject, table = this.table, - self = this; + self = this, + metadatas = []; - if (handle) { - handle.promiseTelemetryObjects().then(function () { - table.buildColumns(handle.getMetadata()); - - self.filterColumns(); - - // 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) - )); - }); + function addMetadata(object) { + if (object.hasCapability('telemetry') && + object.getCapability('telemetry').getMetadata()){ + metadatas.push(object.getCapability('telemetry').getMetadata()); + } } - }; - /** - * @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)); + 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(); + }); + } else { + //Build columns based on collected metadata + buildAndFilterColumns(); + } }; /** @@ -191,11 +226,9 @@ define( * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function (columnConfig) { - if (!columnConfig){ - columnConfig = this.table.getColumnConfiguration(); - this.table.saveColumnConfiguration(columnConfig); - } + TelemetryTableController.prototype.filterColumns = function () { + var 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 59911d1771..5fbab46cf0 100644 --- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js @@ -89,20 +89,24 @@ 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', @@ -113,6 +117,7 @@ define( // used by mocks mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); + mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandle.getDatum.andReturn({}); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ @@ -123,6 +128,8 @@ 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 () { @@ -130,14 +137,16 @@ define( expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true); }); - describe('receives new telemetry', function () { + describe('when receiving 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 03f62f11e3..a872fa63e9 100644 --- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -33,7 +33,13 @@ define( mockTelemetryHandler, mockTelemetryHandle, mockTelemetryFormatter, + mockTelemetryCapability, mockDomainObject, + mockChild, + mockMutationCapability, + mockCompositionCapability, + childMutationCapability, + capabilities = {}, mockTable, mockConfiguration, watches, @@ -64,6 +70,17 @@ 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, @@ -81,13 +98,36 @@ 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; @@ -103,6 +143,7 @@ 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' @@ -127,7 +168,6 @@ define( }); describe('the controller makes use of the table', function () { - it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); @@ -199,14 +239,6 @@ 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(); @@ -214,12 +246,21 @@ define( watches['telemetry:display:bounds'](); expect(controller.subscribe).toHaveBeenCalled(); }); + it('Listens for changes to object model', function () { + controller.registerChangeListeners(); + expect(mockMutationCapability.listen).toHaveBeenCalled(); + }); - 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'](); + 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](); expect(controller.filterColumns).toHaveBeenCalled(); });