From 0c00061cbce7c38dae1c71abe01ea0647eecc4d7 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 30 Mar 2016 14:25:53 -0700 Subject: [PATCH] Added mutation listener --- .../features/table/src/TableConfiguration.js | 27 +++++- .../src/controllers/MCTTableController.js | 20 ++-- .../controllers/RTTelemetryTableController.js | 19 ++-- .../controllers/TelemetryTableController.js | 97 +++++++++++-------- .../RTTelemetryTableControllerSpec.js | 9 +- .../TelemetryTableControllerSpec.js | 71 +++++++++++--- 6 files changed, 164 insertions(+), 79 deletions(-) diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 486f9ab9a6..32c0a69601 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,32 @@ define( {}).columns || {}; }; + function equal(obj1, obj2) { + var obj1Keys = Object.keys(obj1), + obj2Keys = Object.keys(obj2); + return (obj1Keys.length === obj2Keys.length) && + obj1Keys.every(function(key){ + //To do a deep equals, could recurse here if typeof + // obj1 === Object + return obj1[key] === obj2[key]; + }); + } + /** * Set the established configuration on the domain object * @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; + //Don't bother mutating if column configuration is unchanged + if (!equal(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 ba0bc5a334..2ed3e005cd 100644 --- a/platform/features/table/src/controllers/RTTelemetryTableController.js +++ b/platform/features/table/src/controllers/RTTelemetryTableController.js @@ -87,20 +87,15 @@ define( 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); } }); } diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index a9a85af143..6131521d8d 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -57,9 +57,8 @@ define( this.table = new TableConfiguration($scope.domainObject, telemetryFormatter); this.changeListeners = []; - this.initialized = false; - $scope.rows = undefined; + $scope.rows = []; // Subscribe to telemetry when a domain object becomes available this.$scope.$watch('domainObject', function(domainObject){ @@ -86,14 +85,22 @@ define( return listener && listener(); }); this.changeListeners = []; - // When composition changes, re-subscribe to the various - // telemetry subscriptions - this.changeListeners.push(this.$scope.$watchCollection( - 'domainObject.getModel().composition', function(composition, oldComposition) { - if (composition!== oldComposition) { - self.subscribe(); - } - })); + + /** + * 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', @@ -117,8 +124,7 @@ define( */ TelemetryTableController.prototype.subscribe = function () { var self = this; - this.initialized = false; - this.$scope.rows = undefined; + this.$scope.rows = []; if (this.handle) { this.handle.unsubscribe(); @@ -126,10 +132,10 @@ define( //Noop because not supporting realtime data right now function update(){ - if(!self.initialized){ - self.setup(); + //Is there anything to show? + if (self.table.columns.length > 0){ + self.updateRealtime(); } - self.updateRealtime(); } this.handle = this.$scope.domainObject && this.telemetryHandler.handle( @@ -137,8 +143,10 @@ define( update, true // Lossless ); - this.handle.request({}).then(this.addHistoricalData.bind(this)); + + //Call setup at least once + this.setup(); }; /** @@ -175,33 +183,44 @@ define( */ TelemetryTableController.prototype.setup = function () { var handle = this.handle, + domainObject = this.$scope.domainObject, table = this.table, - self = this; + self = this, + metadatas = []; - //Is metadata available yet? - if (handle) { - 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) - )); - self.initialized = true; + 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(); + } + } + + //if (handle) { + //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(); + } + // } }; /** diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js index e5eb968c31..5fbab46cf0 100644 --- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js @@ -89,16 +89,19 @@ 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; @@ -125,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 () { @@ -132,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 62cb859cdb..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'](["one"], ["two"]); - 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(); });