diff --git a/example/generator/StateGeneratorProvider.js b/example/generator/StateGeneratorProvider.js index 09b30dcb69..3733d03a3b 100644 --- a/example/generator/StateGeneratorProvider.js +++ b/example/generator/StateGeneratorProvider.js @@ -63,7 +63,7 @@ define([ StateGeneratorProvider.prototype.request = function (domainObject, options) { var start = options.start; - var end = options.end; + var end = Math.min(Date.now(), options.end); // no future values var duration = domainObject.telemetry.duration * 1000; if (options.strategy === 'latest' || options.size === 1) { start = end; diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index 1e9e84fb32..31958a0686 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -20,6 +20,8 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +const { TelemetryCollection } = require("./TelemetryCollection"); + define([ '../../plugins/displayLayout/CustomStringFormatter', './TelemetryMetadataManager', @@ -273,6 +275,28 @@ define([ } }; + /** + * Request telemetry collection for a domain object. + * The `options` argument allows you to specify filters + * (start, end, etc.), sort order, and strategies for retrieving + * telemetry (aggregation, latest available, etc.). + * + * @method requestTelemetryCollection + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this telemetry collection request + * @returns {TelemetryCollection} a TelemetryCollection instance + */ + TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject, options = {}) { + return new TelemetryCollection( + this.openmct, + domainObject, + options + ); + }; + /** * Request historical telemetry for a domain object. * The `options` argument allows you to specify filters diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js index 09d2fe3591..ff83d8d25d 100644 --- a/src/api/telemetry/TelemetryAPISpec.js +++ b/src/api/telemetry/TelemetryAPISpec.js @@ -19,233 +19,238 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import TelemetryAPI from './TelemetryAPI'; +const { TelemetryCollection } = require("./TelemetryCollection"); -define([ - './TelemetryAPI' -], function ( - TelemetryAPI -) { - xdescribe('Telemetry API', function () { - let openmct; - let telemetryAPI; - let mockTypeService; +describe('Telemetry API', function () { + const NO_PROVIDER = 'No provider found'; + let openmct; + let telemetryAPI; + let mockTypeService; + + beforeEach(function () { + openmct = { + time: jasmine.createSpyObj('timeAPI', [ + 'timeSystem', + 'bounds' + ]), + $injector: jasmine.createSpyObj('injector', [ + 'get' + ]) + }; + mockTypeService = jasmine.createSpyObj('typeService', [ + 'getType' + ]); + openmct.$injector.get.and.returnValue(mockTypeService); + openmct.time.timeSystem.and.returnValue({key: 'system'}); + openmct.time.bounds.and.returnValue({ + start: 0, + end: 1 + }); + telemetryAPI = new TelemetryAPI(openmct); + + }); + + describe('telemetry providers', function () { + let telemetryProvider; + let domainObject; beforeEach(function () { - openmct = { - time: jasmine.createSpyObj('timeAPI', [ - 'timeSystem', - 'bounds' - ]), - $injector: jasmine.createSpyObj('injector', [ - 'get' - ]) - }; - mockTypeService = jasmine.createSpyObj('typeService', [ - 'getType' + telemetryProvider = jasmine.createSpyObj('telemetryProvider', [ + 'supportsSubscribe', + 'subscribe', + 'supportsRequest', + 'request' ]); - openmct.$injector.get.and.returnValue(mockTypeService); - openmct.time.timeSystem.and.returnValue({key: 'system'}); - openmct.time.bounds.and.returnValue({ - start: 0, - end: 1 - }); - telemetryAPI = new TelemetryAPI(openmct); - + domainObject = { + identifier: { + key: 'a', + namespace: 'b' + }, + type: 'sample-type' + }; }); - describe('telemetry providers', function () { - let telemetryProvider; - let domainObject; + it('provides consistent results without providers', function (done) { + const unsubscribe = telemetryAPI.subscribe(domainObject); - beforeEach(function () { - telemetryProvider = jasmine.createSpyObj('telemetryProvider', [ - 'supportsSubscribe', - 'subscribe', - 'supportsRequest', - 'request' - ]); - domainObject = { - identifier: { - key: 'a', - namespace: 'b' - }, - type: 'sample-type' - }; - }); + expect(unsubscribe).toEqual(jasmine.any(Function)); - it('provides consistent results without providers', function () { - const unsubscribe = telemetryAPI.subscribe(domainObject); - expect(unsubscribe).toEqual(jasmine.any(Function)); + telemetryAPI.request(domainObject).then( + () => {}, + (error) => { + expect(error).toBe(NO_PROVIDER); + } + ).finally(done); + }); - const response = telemetryAPI.request(domainObject); - expect(response).toEqual(jasmine.any(Promise)); - }); + it('skips providers that do not match', function (done) { + telemetryProvider.supportsSubscribe.and.returnValue(false); + telemetryProvider.supportsRequest.and.returnValue(false); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); - it('skips providers that do not match', function () { - telemetryProvider.supportsSubscribe.and.returnValue(false); - telemetryProvider.supportsRequest.and.returnValue(false); - telemetryAPI.addProvider(telemetryProvider); + const callback = jasmine.createSpy('callback'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.supportsSubscribe) + .toHaveBeenCalledWith(domainObject); + expect(telemetryProvider.subscribe).not.toHaveBeenCalled(); + expect(unsubscribe).toEqual(jasmine.any(Function)); - const callback = jasmine.createSpy('callback'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.supportsSubscribe) - .toHaveBeenCalledWith(domainObject); - expect(telemetryProvider.subscribe).not.toHaveBeenCalled(); - expect(unsubscribe).toEqual(jasmine.any(Function)); - - const response = telemetryAPI.request(domainObject); + telemetryAPI.request(domainObject).then((response) => { expect(telemetryProvider.supportsRequest) .toHaveBeenCalledWith(domainObject, jasmine.any(Object)); expect(telemetryProvider.request).not.toHaveBeenCalled(); - expect(response).toEqual(jasmine.any(Promise)); + }, (error) => { + expect(error).toBe(NO_PROVIDER); + }).finally(done); + }); + + it('sends subscribe calls to matching providers', function () { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1); + expect(telemetryProvider.supportsSubscribe) + .toHaveBeenCalledWith(domainObject); + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + expect(telemetryProvider.subscribe) + .toHaveBeenCalledWith(domainObject, jasmine.any(Function), undefined); + + const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; + notify('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); + + expect(unsubscribe).toEqual(jasmine.any(Function)); + expect(unsubFunc).not.toHaveBeenCalled(); + unsubscribe(); + expect(unsubFunc).toHaveBeenCalled(); + + notify('otherValue'); + expect(callback).not.toHaveBeenCalledWith('otherValue'); + }); + + it('subscribes once per object', function () { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribetwo = telemetryAPI.subscribe(domainObject, callbacktwo); + + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + + const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; + notify('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); + expect(callbacktwo).toHaveBeenCalledWith('someValue'); + + unsubscribe(); + expect(unsubFunc).not.toHaveBeenCalled(); + notify('otherValue'); + expect(callback).not.toHaveBeenCalledWith('otherValue'); + expect(callbacktwo).toHaveBeenCalledWith('otherValue'); + + unsubscribetwo(); + expect(unsubFunc).toHaveBeenCalled(); + notify('anotherValue'); + expect(callback).not.toHaveBeenCalledWith('anotherValue'); + expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue'); + }); + + it('only deletes subscription cache when there are no more subscribers', function () { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); + const callbackThree = jasmine.createSpy('callback three'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo); + + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribe(); + const unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree); + // Regression test for where subscription cache was deleted on each unsubscribe, resulting in + // superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe, + // then a subsequent subscribe will result in a new subscription at the provider. + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribeTwo(); + unsubscribeThree(); + }); + + it('does subscribe/unsubscribe', function () { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + let unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribe(); + + unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.subscribe.calls.count()).toBe(2); + unsubscribe(); + }); + + it('subscribes for different object', function () { + const unsubFuncs = []; + const notifiers = []; + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryProvider.subscribe.and.callFake(function (obj, cb) { + const unsubFunc = jasmine.createSpy('unsubscribe ' + unsubFuncs.length); + unsubFuncs.push(unsubFunc); + notifiers.push(cb); + + return unsubFunc; }); + telemetryAPI.addProvider(telemetryProvider); - it('sends subscribe calls to matching providers', function () { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); + const otherDomainObject = JSON.parse(JSON.stringify(domainObject)); + otherDomainObject.identifier.namespace = 'other'; - const callback = jasmine.createSpy('callback'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1); - expect(telemetryProvider.supportsSubscribe) - .toHaveBeenCalledWith(domainObject); - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - expect(telemetryProvider.subscribe) - .toHaveBeenCalledWith(domainObject, jasmine.any(Function)); + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); - const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; - notify('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribetwo = telemetryAPI.subscribe(otherDomainObject, callbacktwo); - expect(unsubscribe).toEqual(jasmine.any(Function)); - expect(unsubFunc).not.toHaveBeenCalled(); - unsubscribe(); - expect(unsubFunc).toHaveBeenCalled(); + expect(telemetryProvider.subscribe.calls.count()).toBe(2); - notify('otherValue'); - expect(callback).not.toHaveBeenCalledWith('otherValue'); - }); + notifiers[0]('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); + expect(callbacktwo).not.toHaveBeenCalledWith('someValue'); - it('subscribes once per object', function () { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); + notifiers[1]('anotherValue'); + expect(callback).not.toHaveBeenCalledWith('anotherValue'); + expect(callbacktwo).toHaveBeenCalledWith('anotherValue'); - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribetwo = telemetryAPI.subscribe(domainObject, callbacktwo); + unsubscribe(); + expect(unsubFuncs[0]).toHaveBeenCalled(); + expect(unsubFuncs[1]).not.toHaveBeenCalled(); - expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribetwo(); + expect(unsubFuncs[1]).toHaveBeenCalled(); + }); - const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; - notify('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); - expect(callbacktwo).toHaveBeenCalledWith('someValue'); + it('sends requests to matching providers', function (done) { + const telemPromise = Promise.resolve([]); + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(telemPromise); + telemetryAPI.addProvider(telemetryProvider); - unsubscribe(); - expect(unsubFunc).not.toHaveBeenCalled(); - notify('otherValue'); - expect(callback).not.toHaveBeenCalledWith('otherValue'); - expect(callbacktwo).toHaveBeenCalledWith('otherValue'); - - unsubscribetwo(); - expect(unsubFunc).toHaveBeenCalled(); - notify('anotherValue'); - expect(callback).not.toHaveBeenCalledWith('anotherValue'); - expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue'); - }); - - it('only deletes subscription cache when there are no more subscribers', function () { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - const callbackThree = jasmine.createSpy('callback three'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo); - - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribe(); - const unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree); - // Regression test for where subscription cache was deleted on each unsubscribe, resulting in - // superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe, - // then a subsequent subscribe will result in a new subscription at the provider. - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribeTwo(); - unsubscribeThree(); - }); - - it('does subscribe/unsubscribe', function () { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - let unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribe(); - - unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.subscribe.calls.count()).toBe(2); - unsubscribe(); - }); - - it('subscribes for different object', function () { - const unsubFuncs = []; - const notifiers = []; - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryProvider.subscribe.and.callFake(function (obj, cb) { - const unsubFunc = jasmine.createSpy('unsubscribe ' + unsubFuncs.length); - unsubFuncs.push(unsubFunc); - notifiers.push(cb); - - return unsubFunc; - }); - telemetryAPI.addProvider(telemetryProvider); - - const otherDomainObject = JSON.parse(JSON.stringify(domainObject)); - otherDomainObject.identifier.namespace = 'other'; - - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribetwo = telemetryAPI.subscribe(otherDomainObject, callbacktwo); - - expect(telemetryProvider.subscribe.calls.count()).toBe(2); - - notifiers[0]('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); - expect(callbacktwo).not.toHaveBeenCalledWith('someValue'); - - notifiers[1]('anotherValue'); - expect(callback).not.toHaveBeenCalledWith('anotherValue'); - expect(callbacktwo).toHaveBeenCalledWith('anotherValue'); - - unsubscribe(); - expect(unsubFuncs[0]).toHaveBeenCalled(); - expect(unsubFuncs[1]).not.toHaveBeenCalled(); - - unsubscribetwo(); - expect(unsubFuncs[1]).toHaveBeenCalled(); - }); - - it('sends requests to matching providers', function () { - const telemPromise = Promise.resolve([]); - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryProvider.request.and.returnValue(telemPromise); - telemetryAPI.addProvider(telemetryProvider); - - const result = telemetryAPI.request(domainObject); - expect(result).toBe(telemPromise); + telemetryAPI.request(domainObject).then(() => { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( domainObject, jasmine.any(Object) @@ -254,13 +259,15 @@ define([ domainObject, jasmine.any(Object) ); - }); + }).finally(done); + }); - it('generates default request options', function () { - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); + it('generates default request options', function (done) { + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); - telemetryAPI.request(domainObject); + telemetryAPI.request(domainObject).then(() => { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( jasmine.any(Object), { @@ -282,36 +289,39 @@ define([ telemetryProvider.supportsRequest.calls.reset(); telemetryProvider.request.calls.reset(); - telemetryAPI.request(domainObject, {}); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( - jasmine.any(Object), - { - start: 0, - end: 1, - domain: 'system' - } - ); + telemetryAPI.request(domainObject, {}).then(() => { + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( + jasmine.any(Object), + { + start: 0, + end: 1, + domain: 'system' + } + ); - expect(telemetryProvider.request).toHaveBeenCalledWith( - jasmine.any(Object), - { - start: 0, - end: 1, - domain: 'system' - } - ); - }); - - it('does not overwrite existing request options', function () { - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - telemetryAPI.request(domainObject, { - start: 20, - end: 30, - domain: 'someDomain' + expect(telemetryProvider.request).toHaveBeenCalledWith( + jasmine.any(Object), + { + start: 0, + end: 1, + domain: 'system' + } + ); }); + }).finally(done); + }); + + it('do not overwrite existing request options', function (done) { + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); + + telemetryAPI.request(domainObject, { + start: 20, + end: 30, + domain: 'someDomain' + }).then(() => { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( jasmine.any(Object), { @@ -329,235 +339,275 @@ define([ domain: 'someDomain' } ); + + }).finally(done); + }); + }); + + describe('metadata', function () { + let mockMetadata = {}; + let mockObjectType = { + typeDef: {} + }; + beforeEach(function () { + telemetryAPI.addProvider({ + key: 'mockMetadataProvider', + supportsMetadata() { + return true; + }, + getMetadata() { + return mockMetadata; + } + }); + mockTypeService.getType.and.returnValue(mockObjectType); + }); + + it('respects explicit priority', function () { + mockMetadata.values = [ + { + key: "name", + name: "Name", + hints: { + priority: 2 + } + + }, + { + key: "timestamp", + name: "Timestamp", + hints: { + priority: 1 + } + }, + { + key: "sin", + name: "Sine", + hints: { + priority: 4 + } + }, + { + key: "cos", + name: "Cosine", + hints: { + priority: 3 + } + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.values(); + + values.forEach((value, index) => { + expect(value.hints.priority).toBe(index + 1); }); }); - describe('metadata', function () { - let mockMetadata = {}; - let mockObjectType = { - typeDef: {} - }; - beforeEach(function () { - telemetryAPI.addProvider({ - key: 'mockMetadataProvider', - supportsMetadata() { - return true; - }, - getMetadata() { - return mockMetadata; - } - }); - mockTypeService.getType.and.returnValue(mockObjectType); + it('if no explicit priority, defaults to order defined', function () { + mockMetadata.values = [ + { + key: "name", + name: "Name" + + }, + { + key: "timestamp", + name: "Timestamp" + }, + { + key: "sin", + name: "Sine" + }, + { + key: "cos", + name: "Cosine" + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.values(); + + values.forEach((value, index) => { + expect(value.key).toBe(mockMetadata.values[index].key); }); - it('respects explicit priority', function () { - mockMetadata.values = [ - { - key: "name", - name: "Name", - hints: { - priority: 2 - } + }); + it('respects domain priority', function () { + mockMetadata.values = [ + { + key: "name", + name: "Name" - }, - { - key: "timestamp", - name: "Timestamp", - hints: { - priority: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - priority: 4 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - priority: 3 - } + }, + { + key: "timestamp-utc", + name: "Timestamp UTC", + hints: { + domain: 2 } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.values(); - - values.forEach((value, index) => { - expect(value.hints.priority).toBe(index + 1); - }); - }); - it('if no explicit priority, defaults to order defined', function () { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp", - name: "Timestamp" - }, - { - key: "sin", - name: "Sine" - }, - { - key: "cos", - name: "Cosine" + }, + { + key: "timestamp-local", + name: "Timestamp Local", + hints: { + domain: 1 } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.values(); - - values.forEach((value, index) => { - expect(value.key).toBe(mockMetadata.values[index].key); - }); - }); - it('respects domain priority', function () { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - range: 2 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - range: 1 - } + }, + { + key: "sin", + name: "Sine", + hints: { + range: 2 } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['domain']); - - expect(values[0].key).toBe('timestamp-local'); - expect(values[1].key).toBe('timestamp-utc'); - }); - it('respects range priority', function () { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - range: 2 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - range: 1 - } + }, + { + key: "cos", + name: "Cosine", + hints: { + range: 1 } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['range']); + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['domain']); - expect(values[0].key).toBe('cos'); - expect(values[1].key).toBe('sin'); - }); - it('respects priority and domain ordering', function () { - mockMetadata.values = [ - { - key: "id", - name: "ID", - hints: { - priority: 2 - } - }, - { - key: "name", - name: "Name", - hints: { - priority: 1 - } + expect(values[0].key).toBe('timestamp-local'); + expect(values[1].key).toBe('timestamp-utc'); + }); + it('respects range priority', function () { + mockMetadata.values = [ + { + key: "name", + name: "Name" - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2, - priority: 1 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1, - priority: 2 - } - }, - { - key: "timestamp-pst", - name: "Timestamp PST", - hints: { - domain: 3, - priority: 2 - } - }, - { - key: "sin", - name: "Sine" - }, - { - key: "cos", - name: "Cosine" + }, + { + key: "timestamp-utc", + name: "Timestamp UTC", + hints: { + domain: 2 } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['priority', 'domain']); - [ - 'timestamp-utc', - 'timestamp-local', - 'timestamp-pst' - ].forEach((key, index) => { - expect(values[index].key).toBe(key); - }); + }, + { + key: "timestamp-local", + name: "Timestamp Local", + hints: { + domain: 1 + } + }, + { + key: "sin", + name: "Sine", + hints: { + range: 2 + } + }, + { + key: "cos", + name: "Cosine", + hints: { + range: 1 + } + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['range']); + + expect(values[0].key).toBe('cos'); + expect(values[1].key).toBe('sin'); + }); + it('respects priority and domain ordering', function () { + mockMetadata.values = [ + { + key: "id", + name: "ID", + hints: { + priority: 2 + } + }, + { + key: "name", + name: "Name", + hints: { + priority: 1 + } + + }, + { + key: "timestamp-utc", + name: "Timestamp UTC", + hints: { + domain: 2, + priority: 1 + } + }, + { + key: "timestamp-local", + name: "Timestamp Local", + hints: { + domain: 1, + priority: 2 + } + }, + { + key: "timestamp-pst", + name: "Timestamp PST", + hints: { + domain: 3, + priority: 2 + } + }, + { + key: "sin", + name: "Sine" + }, + { + key: "cos", + name: "Cosine" + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['priority', 'domain']); + [ + 'timestamp-utc', + 'timestamp-local', + 'timestamp-pst' + ].forEach((key, index) => { + expect(values[index].key).toBe(key); }); }); }); + + describe('telemetry collections', () => { + let domainObject; + let mockMetadata = {}; + let mockObjectType = { + typeDef: {} + }; + + beforeEach(function () { + openmct.telemetry = telemetryAPI; + telemetryAPI.addProvider({ + key: 'mockMetadataProvider', + supportsMetadata() { + return true; + }, + getMetadata() { + return mockMetadata; + } + }); + mockTypeService.getType.and.returnValue(mockObjectType); + domainObject = { + identifier: { + key: 'a', + namespace: 'b' + }, + type: 'sample-type' + }; + }); + + it('when requested, returns an instance of telemetry collection', () => { + const telemetryCollection = telemetryAPI.requestTelemetryCollection(domainObject); + + expect(telemetryCollection).toBeInstanceOf(TelemetryCollection); + }); + + }); }); + diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js new file mode 100644 index 0000000000..33e279476d --- /dev/null +++ b/src/api/telemetry/TelemetryCollection.js @@ -0,0 +1,366 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +import _ from 'lodash'; +import EventEmitter from 'EventEmitter'; + +/** Class representing a Telemetry Collection. */ + +export class TelemetryCollection extends EventEmitter { + /** + * Creates a Telemetry Collection + * + * @param {object} openmct - Openm MCT + * @param {object} domainObject - Domain Object to user for telemetry collection + * @param {object} options - Any options passed in for request/subscribe + */ + constructor(openmct, domainObject, options) { + super(); + + this.loaded = false; + this.openmct = openmct; + this.domainObject = domainObject; + this.boundedTelemetry = []; + this.futureBuffer = []; + this.parseTime = undefined; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this.unsubscribe = undefined; + this.historicalProvider = undefined; + this.options = options; + this.pageState = undefined; + this.lastBounds = undefined; + this.requestAbort = undefined; + } + + /** + * This will start the requests for historical and realtime data, + * as well as setting up initial values and watchers + */ + load() { + if (this.loaded) { + throw new Error('Telemetry Collection has already been loaded.'); + } + + this._timeSystem(this.openmct.time.timeSystem()); + this.lastBounds = this.openmct.time.bounds(); + + this._watchBounds(); + this._watchTimeSystem(); + + this._initiateHistoricalRequests(); + this._initiateSubscriptionTelemetry(); + + this.loaded = true; + } + + /** + * can/should be called by the requester of the telemetry collection + * to remove any listeners + */ + destroy() { + if (this.requestAbort) { + this.requestAbort.abort(); + } + + this._unwatchBounds(); + this._unwatchTimeSystem(); + if (this.unsubscribe) { + this.unsubscribe(); + } + + this.removeAllListeners(); + } + + /** + * This will start the requests for historical and realtime data, + * as well as setting up initial values and watchers + */ + getAll() { + return this.boundedTelemetry; + } + + /** + * Sets up the telemetry collection for historical requests, + * this uses the "standardizeRequestOptions" from Telemetry API + * @private + */ + _initiateHistoricalRequests() { + this.openmct.telemetry.standardizeRequestOptions(this.options); + this.historicalProvider = this.openmct.telemetry. + findRequestProvider(this.domainObject, this.options); + + this._requestHistoricalTelemetry(); + } + /** + * If a historical provider exists, then historical requests will be made + * @private + */ + async _requestHistoricalTelemetry() { + if (!this.historicalProvider) { + return; + } + + let historicalData; + + try { + this.requestAbort = new AbortController(); + this.options.abortSignal = this.requestAbort.signal; + historicalData = await this.historicalProvider.request(this.domainObject, this.options); + this.requestAbort = undefined; + } catch (error) { + console.error('Error requesting telemetry data...'); + this.requestAbort = undefined; + throw new Error(error); + } + + this._processNewTelemetry(historicalData); + + } + /** + * This uses the built in subscription function from Telemetry API + * @private + */ + _initiateSubscriptionTelemetry() { + + if (this.unsubscribe) { + this.unsubscribe(); + } + + this.unsubscribe = this.openmct.telemetry + .subscribe( + this.domainObject, + datum => this._processNewTelemetry(datum), + this.options + ); + } + + /** + * Filter any new telemetry (add/page, historical, subscription) based on + * time bounds and dupes + * + * @param {(Object|Object[])} telemetryData - telemetry data object or + * array of telemetry data objects + * @private + */ + _processNewTelemetry(telemetryData) { + let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; + let parsedValue; + let beforeStartOfBounds; + let afterEndOfBounds; + let added = []; + + for (let datum of data) { + parsedValue = this.parseTime(datum); + beforeStartOfBounds = parsedValue < this.lastBounds.start; + afterEndOfBounds = parsedValue > this.lastBounds.end; + + if (!afterEndOfBounds && !beforeStartOfBounds) { + let isDuplicate = false; + let startIndex = this._sortedIndex(datum); + let endIndex = undefined; + + // dupe check + if (startIndex !== this.boundedTelemetry.length) { + endIndex = _.sortedLastIndexBy( + this.boundedTelemetry, + datum, + boundedDatum => this.parseTime(boundedDatum) + ); + + if (endIndex > startIndex) { + let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); + + isDuplicate = potentialDupes.some(_.isEqual(undefined, datum)); + } + } + + if (!isDuplicate) { + let index = endIndex || startIndex; + + this.boundedTelemetry.splice(index, 0, datum); + added.push(datum); + } + + } else if (afterEndOfBounds) { + this.futureBuffer.push(datum); + } + } + + if (added.length) { + this.emit('add', added); + } + } + + /** + * Finds the correct insertion point for the given telemetry datum. + * Leverages lodash's `sortedIndexBy` function which implements a binary search. + * @private + */ + _sortedIndex(datum) { + if (this.boundedTelemetry.length === 0) { + return 0; + } + + let parsedValue = this.parseTime(datum); + let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]); + + if (parsedValue > lastValue || parsedValue === lastValue) { + return this.boundedTelemetry.length; + } else { + return _.sortedIndexBy( + this.boundedTelemetry, + datum, + boundedDatum => this.parseTime(boundedDatum) + ); + } + } + + /** + * when the start time, end time, or both have been updated. + * data could be added OR removed here we update the current + * bounded telemetry + * + * @param {TimeConductorBounds} bounds The newly updated bounds + * @param {boolean} [tick] `true` if the bounds update was due to + * a "tick" event (ie. was an automatic update), false otherwise. + * @private + */ + _bounds(bounds, isTick) { + let startChanged = this.lastBounds.start !== bounds.start; + let endChanged = this.lastBounds.end !== bounds.end; + + this.lastBounds = bounds; + + if (isTick) { + // need to check futureBuffer and need to check + // if anything has fallen out of bounds + let startIndex = 0; + let endIndex = 0; + + let discarded = []; + let added = []; + let testDatum = {}; + + if (startChanged) { + testDatum[this.timeKey] = bounds.start; + // Calculate the new index of the first item within the bounds + startIndex = _.sortedIndexBy( + this.boundedTelemetry, + testDatum, + datum => this.parseTime(datum) + ); + discarded = this.boundedTelemetry.splice(0, startIndex); + } + + if (endChanged) { + testDatum[this.timeKey] = bounds.end; + // Calculate the new index of the last item in bounds + endIndex = _.sortedLastIndexBy( + this.futureBuffer, + testDatum, + datum => this.parseTime(datum) + ); + added = this.futureBuffer.splice(0, endIndex); + this.boundedTelemetry = [...this.boundedTelemetry, ...added]; + } + + if (discarded.length > 0) { + this.emit('remove', discarded); + } + + if (added.length > 0) { + this.emit('add', added); + } + + } else { + // user bounds change, reset + this._reset(); + } + + } + + /** + * whenever the time system is updated need to update related values in + * the Telemetry Collection and reset the telemetry collection + * + * @param {TimeSystem} timeSystem - the value of the currently applied + * Time System + * @private + */ + _timeSystem(timeSystem) { + this.timeKey = timeSystem.key; + let metadataValue = this.metadata.value(this.timeKey) || { format: this.timeKey }; + let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + + this.parseTime = (datum) => { + return valueFormatter.parse(datum); + }; + + this._reset(); + } + + /** + * Reset the telemetry data of the collection, and re-request + * historical telemetry + * @private + * + * @todo handle subscriptions more granually + */ + _reset() { + this.boundedTelemetry = []; + this.futureBuffer = []; + + this._requestHistoricalTelemetry(); + } + + /** + * adds the _bounds callback to the 'bounds' timeAPI listener + * @private + */ + _watchBounds() { + this.openmct.time.on('bounds', this._bounds, this); + } + + /** + * removes the _bounds callback from the 'bounds' timeAPI listener + * @private + */ + _unwatchBounds() { + this.openmct.time.off('bounds', this._bounds, this); + } + + /** + * adds the _timeSystem callback to the 'timeSystem' timeAPI listener + * @private + */ + _watchTimeSystem() { + this.openmct.time.on('timeSystem', this._timeSystem, this); + } + + /** + * removes the _timeSystem callback from the 'timeSystem' timeAPI listener + * @private + */ + _unwatchTimeSystem() { + this.openmct.time.off('timeSystem', this._timeSystem, this); + } +} diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index f69a1f1995..180e06a500 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -23,20 +23,18 @@ define([ 'EventEmitter', 'lodash', - './collections/BoundedTableRowCollection', - './collections/FilteredTableRowCollection', - './TelemetryTableNameColumn', + './collections/TableRowCollection', './TelemetryTableRow', + './TelemetryTableNameColumn', './TelemetryTableColumn', './TelemetryTableUnitColumn', './TelemetryTableConfiguration' ], function ( EventEmitter, _, - BoundedTableRowCollection, - FilteredTableRowCollection, - TelemetryTableNameColumn, + TableRowCollection, TelemetryTableRow, + TelemetryTableNameColumn, TelemetryTableColumn, TelemetryTableUnitColumn, TelemetryTableConfiguration @@ -48,20 +46,23 @@ define([ this.domainObject = domainObject; this.openmct = openmct; this.rowCount = 100; - this.subscriptions = {}; this.tableComposition = undefined; - this.telemetryObjects = []; this.datumCache = []; - this.outstandingRequests = 0; this.configuration = new TelemetryTableConfiguration(domainObject, openmct); this.paused = false; this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.telemetryObjects = {}; + this.telemetryCollections = {}; + this.delayedActions = []; + this.outstandingRequests = 0; + this.addTelemetryObject = this.addTelemetryObject.bind(this); this.removeTelemetryObject = this.removeTelemetryObject.bind(this); + this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); + this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); this.isTelemetryObject = this.isTelemetryObject.bind(this); this.refreshData = this.refreshData.bind(this); - this.requestDataFor = this.requestDataFor.bind(this); this.updateFilters = this.updateFilters.bind(this); this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); @@ -102,8 +103,7 @@ define([ } createTableRowCollections() { - this.boundedRows = new BoundedTableRowCollection(this.openmct); - this.filteredRows = new FilteredTableRowCollection(this.boundedRows); + this.tableRows = new TableRowCollection(); //Fetch any persisted default sort let sortOptions = this.configuration.getConfiguration().sortOptions; @@ -113,11 +113,14 @@ define([ key: this.openmct.time.timeSystem().key, direction: 'asc' }; - this.filteredRows.sortBy(sortOptions); + + this.tableRows.sortBy(sortOptions); + this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); } loadComposition() { this.tableComposition = this.openmct.composition.get(this.domainObject); + if (this.tableComposition !== undefined) { this.tableComposition.load().then((composition) => { @@ -132,66 +135,64 @@ define([ addTelemetryObject(telemetryObject) { this.addColumnsForObject(telemetryObject, true); - this.requestDataFor(telemetryObject); - this.subscribeTo(telemetryObject); - this.telemetryObjects.push(telemetryObject); + + const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); + let columnMap = this.getColumnMapForObject(keyString); + let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + + this.incrementOutstandingRequests(); + + const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); + const telemetryRemover = this.getTelemetryRemover(); + + this.removeTelemetryCollection(keyString); + + this.telemetryCollections[keyString] = this.openmct.telemetry + .requestTelemetryCollection(telemetryObject, requestOptions); + + this.telemetryCollections[keyString].on('remove', telemetryRemover); + this.telemetryCollections[keyString].on('add', telemetryProcessor); + this.telemetryCollections[keyString].load(); + + this.decrementOutstandingRequests(); + + this.telemetryObjects[keyString] = { + telemetryObject, + keyString, + requestOptions, + columnMap, + limitEvaluator + }; this.emit('object-added', telemetryObject); } - updateFilters(updatedFilters) { - let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + getTelemetryProcessor(keyString, columnMap, limitEvaluator) { + return (telemetry) => { + //Check that telemetry object has not been removed since telemetry was requested. + if (!this.telemetryObjects[keyString]) { + return; + } - if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { - this.filters = deepCopiedFilters; - this.clearAndResubscribe(); - } else { - this.filters = deepCopiedFilters; - } + let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + + if (this.paused) { + this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add')); + } else { + this.tableRows.addRows(telemetryRows, 'add'); + } + }; } - clearAndResubscribe() { - this.filteredRows.clear(); - this.boundedRows.clear(); - Object.keys(this.subscriptions).forEach(this.unsubscribe, this); - - this.telemetryObjects.forEach(this.requestDataFor.bind(this)); - this.telemetryObjects.forEach(this.subscribeTo.bind(this)); - } - - removeTelemetryObject(objectIdentifier) { - this.configuration.removeColumnsForObject(objectIdentifier, true); - let keyString = this.openmct.objects.makeKeyString(objectIdentifier); - this.boundedRows.removeAllRowsForObject(keyString); - this.unsubscribe(keyString); - this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(objectIdentifier, object.identifier)); - - this.emit('object-removed', objectIdentifier); - } - - requestDataFor(telemetryObject) { - this.incrementOutstandingRequests(); - let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); - - return this.openmct.telemetry.request(telemetryObject, requestOptions) - .then(telemetryData => { - //Check that telemetry object has not been removed since telemetry was requested. - if (!this.telemetryObjects.includes(telemetryObject)) { - return; - } - - let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let columnMap = this.getColumnMapForObject(keyString); - let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); - this.processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator); - }).finally(() => { - this.decrementOutstandingRequests(); - }); - } - - processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) { - let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - this.boundedRows.add(telemetryRows); + getTelemetryRemover() { + return (telemetry) => { + if (this.paused) { + this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry)); + } else { + this.tableRows.removeRowsByData(telemetry); + } + }; } /** @@ -216,35 +217,72 @@ define([ } } + // will pull all necessary information for all existing bounded telemetry + // and pass to table row collection to reset without making any new requests + // triggered by filtering + resetRowsFromAllData() { + let allRows = []; + + Object.keys(this.telemetryCollections).forEach(keyString => { + let { columnMap, limitEvaluator } = this.telemetryObjects[keyString]; + + this.telemetryCollections[keyString].getAll().forEach(datum => { + allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + }); + }); + + this.tableRows.addRows(allRows, 'filter'); + } + + updateFilters(updatedFilters) { + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.tableRows.clear(); + this.clearAndResubscribe(); + } else { + this.filters = deepCopiedFilters; + } + } + + clearAndResubscribe() { + let objectKeys = Object.keys(this.telemetryObjects); + + this.tableRows.clear(); + objectKeys.forEach((keyString) => { + this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject); + }); + } + + removeTelemetryObject(objectIdentifier) { + const keyString = this.openmct.objects.makeKeyString(objectIdentifier); + + this.configuration.removeColumnsForObject(objectIdentifier, true); + this.tableRows.removeRowsByObject(keyString); + + this.removeTelemetryCollection(keyString); + delete this.telemetryObjects[keyString]; + + this.emit('object-removed', objectIdentifier); + } + refreshData(bounds, isTick) { - if (!isTick && this.outstandingRequests === 0) { - this.filteredRows.clear(); - this.boundedRows.clear(); - this.boundedRows.sortByTimeSystem(this.openmct.time.timeSystem()); - this.telemetryObjects.forEach(this.requestDataFor); + if (!isTick && this.tableRows.outstandingRequests === 0) { + this.tableRows.clear(); + this.tableRows.sortBy({ + key: this.openmct.time.timeSystem().key, + direction: 'asc' + }); + this.tableRows.resubscribe(); } } clearData() { - this.filteredRows.clear(); - this.boundedRows.clear(); + this.tableRows.clear(); this.emit('refresh'); } - getColumnMapForObject(objectKeyString) { - let columns = this.configuration.getColumns(); - - if (columns[objectKeyString]) { - return columns[objectKeyString].reduce((map, column) => { - map[column.getKey()] = column; - - return map; - }, {}); - } - - return {}; - } - addColumnsForObject(telemetryObject) { let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); @@ -264,54 +302,18 @@ define([ }); } - createColumn(metadatum) { - return new TelemetryTableColumn(this.openmct, metadatum); - } + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); - createUnitColumn(metadatum) { - return new TelemetryTableUnitColumn(this.openmct, metadatum); - } + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; - subscribeTo(telemetryObject) { - let subscribeOptions = this.buildOptionsFromConfiguration(telemetryObject); - let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let columnMap = this.getColumnMapForObject(keyString); - let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + return map; + }, {}); + } - this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => { - //Check that telemetry object has not been removed since telemetry was requested. - if (!this.telemetryObjects.includes(telemetryObject)) { - return; - } - - if (this.paused) { - let realtimeDatum = { - datum, - columnMap, - keyString, - limitEvaluator - }; - - this.datumCache.push(realtimeDatum); - } else { - this.processRealtimeDatum(datum, columnMap, keyString, limitEvaluator); - } - }, subscribeOptions); - } - - processDatumCache() { - this.datumCache.forEach(cachedDatum => { - this.processRealtimeDatum(cachedDatum.datum, cachedDatum.columnMap, cachedDatum.keyString, cachedDatum.limitEvaluator); - }); - this.datumCache = []; - } - - processRealtimeDatum(datum, columnMap, keyString, limitEvaluator) { - this.boundedRows.add(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - } - - isTelemetryObject(domainObject) { - return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); + return {}; } buildOptionsFromConfiguration(telemetryObject) { @@ -323,13 +325,20 @@ define([ return {filters} || {}; } - unsubscribe(keyString) { - this.subscriptions[keyString](); - delete this.subscriptions[keyString]; + createColumn(metadatum) { + return new TelemetryTableColumn(this.openmct, metadatum); + } + + createUnitColumn(metadatum) { + return new TelemetryTableUnitColumn(this.openmct, metadatum); + } + + isTelemetryObject(domainObject) { + return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); } sortBy(sortOptions) { - this.filteredRows.sortBy(sortOptions); + this.tableRows.sortBy(sortOptions); if (this.openmct.editor.isEditing()) { let configuration = this.configuration.getConfiguration(); @@ -338,21 +347,36 @@ define([ } } + runDelayedActions() { + this.delayedActions.forEach(action => action()); + this.delayedActions = []; + } + + removeTelemetryCollection(keyString) { + if (this.telemetryCollections[keyString]) { + this.telemetryCollections[keyString].destroy(); + this.telemetryCollections[keyString] = undefined; + delete this.telemetryCollections[keyString]; + } + } + pause() { this.paused = true; - this.boundedRows.unsubscribeFromBounds(); } unpause() { this.paused = false; - this.processDatumCache(); - this.boundedRows.subscribeToBounds(); + this.runDelayedActions(); } destroy() { - this.boundedRows.destroy(); - this.filteredRows.destroy(); - Object.keys(this.subscriptions).forEach(this.unsubscribe, this); + this.tableRows.destroy(); + + this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData); + + let keystrings = Object.keys(this.telemetryCollections); + keystrings.forEach(this.removeTelemetryCollection); + this.openmct.time.off('bounds', this.refreshData); this.openmct.time.off('timeSystem', this.refreshData); diff --git a/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js b/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js deleted file mode 100644 index 09d600b655..0000000000 --- a/src/plugins/telemetryTable/collections/BoundedTableRowCollection.js +++ /dev/null @@ -1,166 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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( - [ - 'lodash', - './SortedTableRowCollection' - ], - function ( - _, - SortedTableRowCollection - ) { - - class BoundedTableRowCollection extends SortedTableRowCollection { - constructor(openmct) { - super(); - - this.futureBuffer = new SortedTableRowCollection(); - this.openmct = openmct; - - this.sortByTimeSystem = this.sortByTimeSystem.bind(this); - this.bounds = this.bounds.bind(this); - - this.sortByTimeSystem(openmct.time.timeSystem()); - - this.lastBounds = openmct.time.bounds(); - - this.subscribeToBounds(); - } - - addOne(item) { - let parsedValue = this.getValueForSortColumn(item); - // Insert into either in-bounds array, or the future buffer. - // Data in the future buffer will be re-evaluated for possible - // insertion on next bounds change - let beforeStartOfBounds = parsedValue < this.lastBounds.start; - let afterEndOfBounds = parsedValue > this.lastBounds.end; - - if (!afterEndOfBounds && !beforeStartOfBounds) { - return super.addOne(item); - } else if (afterEndOfBounds) { - this.futureBuffer.addOne(item); - } - - return false; - } - - sortByTimeSystem(timeSystem) { - this.sortBy({ - key: timeSystem.key, - direction: 'asc' - }); - let formatter = this.openmct.telemetry.getValueFormatter({ - key: timeSystem.key, - source: timeSystem.key, - format: timeSystem.timeFormat - }); - this.parseTime = formatter.parse.bind(formatter); - this.futureBuffer.sortBy({ - key: timeSystem.key, - direction: 'asc' - }); - } - - /** - * This function is optimized for ticking - it assumes that start and end - * bounds will only increase and as such this cannot be used for decreasing - * bounds changes. - * - * An implication of this is that data will not be discarded that exceeds - * the given end bounds. For arbitrary bounds changes, it's assumed that - * a telemetry requery is performed anyway, and the collection is cleared - * and repopulated. - * - * @fires TelemetryCollection#added - * @fires TelemetryCollection#discarded - * @param bounds - */ - bounds(bounds) { - let startChanged = this.lastBounds.start !== bounds.start; - let endChanged = this.lastBounds.end !== bounds.end; - - let startIndex = 0; - let endIndex = 0; - - let discarded = []; - let added = []; - let testValue = { - datum: {} - }; - - this.lastBounds = bounds; - - if (startChanged) { - testValue.datum[this.sortOptions.key] = bounds.start; - // Calculate the new index of the first item within the bounds - startIndex = this.sortedIndex(this.rows, testValue); - discarded = this.rows.splice(0, startIndex); - } - - if (endChanged) { - testValue.datum[this.sortOptions.key] = bounds.end; - // Calculate the new index of the last item in bounds - endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue); - added = this.futureBuffer.rows.splice(0, endIndex); - added.forEach((datum) => this.rows.push(datum)); - } - - if (discarded && discarded.length > 0) { - /** - * A `discarded` event is emitted when telemetry data fall out of - * bounds due to a bounds change event - * @type {object[]} discarded the telemetry data - * discarded as a result of the bounds change - */ - this.emit('remove', discarded); - } - - if (added && added.length > 0) { - /** - * An `added` event is emitted when a bounds change results in - * received telemetry falling within the new bounds. - * @type {object[]} added the telemetry data that is now within bounds - */ - this.emit('add', added); - } - } - - getValueForSortColumn(row) { - return this.parseTime(row.datum[this.sortOptions.key]); - } - - unsubscribeFromBounds() { - this.openmct.time.off('bounds', this.bounds); - } - - subscribeToBounds() { - this.openmct.time.on('bounds', this.bounds); - } - - destroy() { - this.unsubscribeFromBounds(); - } - } - - return BoundedTableRowCollection; - }); diff --git a/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js b/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js deleted file mode 100644 index fda46eefd0..0000000000 --- a/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js +++ /dev/null @@ -1,136 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT 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 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( - [ - './SortedTableRowCollection' - ], - function ( - SortedTableRowCollection - ) { - class FilteredTableRowCollection extends SortedTableRowCollection { - constructor(masterCollection) { - super(); - - this.masterCollection = masterCollection; - this.columnFilters = {}; - - //Synchronize with master collection - this.masterCollection.on('add', this.add); - this.masterCollection.on('remove', this.remove); - - //Default to master collection's sort options - this.sortOptions = masterCollection.sortBy(); - } - - setColumnFilter(columnKey, filter) { - filter = filter.trim().toLowerCase(); - - let rowsToFilter = this.getRowsToFilter(columnKey, filter); - - if (filter.length === 0) { - delete this.columnFilters[columnKey]; - } else { - this.columnFilters[columnKey] = filter; - } - - this.rows = rowsToFilter.filter(this.matchesFilters, this); - this.emit('filter'); - } - - setColumnRegexFilter(columnKey, filter) { - filter = filter.trim(); - - let rowsToFilter = this.masterCollection.getRows(); - - this.columnFilters[columnKey] = new RegExp(filter); - this.rows = rowsToFilter.filter(this.matchesFilters, this); - this.emit('filter'); - } - - /** - * @private - */ - getRowsToFilter(columnKey, filter) { - if (this.isSubsetOfCurrentFilter(columnKey, filter)) { - return this.getRows(); - } else { - return this.masterCollection.getRows(); - } - } - - /** - * @private - */ - isSubsetOfCurrentFilter(columnKey, filter) { - if (this.columnFilters[columnKey] instanceof RegExp) { - return false; - } - - return this.columnFilters[columnKey] - && filter.startsWith(this.columnFilters[columnKey]) - // startsWith check will otherwise fail when filter cleared - // because anyString.startsWith('') === true - && filter !== ''; - } - - addOne(row) { - return this.matchesFilters(row) && super.addOne(row); - } - - /** - * @private - */ - matchesFilters(row) { - let doesMatchFilters = true; - Object.keys(this.columnFilters).forEach((key) => { - if (!doesMatchFilters || !this.rowHasColumn(row, key)) { - return false; - } - - let formattedValue = row.getFormattedValue(key); - if (formattedValue === undefined) { - return false; - } - - if (this.columnFilters[key] instanceof RegExp) { - doesMatchFilters = this.columnFilters[key].test(formattedValue); - } else { - doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; - } - }); - - return doesMatchFilters; - } - - rowHasColumn(row, key) { - return Object.prototype.hasOwnProperty.call(row.columns, key); - } - - destroy() { - this.masterCollection.off('add', this.add); - this.masterCollection.off('remove', this.remove); - } - } - - return FilteredTableRowCollection; - }); diff --git a/src/plugins/telemetryTable/collections/SortedTableRowCollection.js b/src/plugins/telemetryTable/collections/TableRowCollection.js similarity index 58% rename from src/plugins/telemetryTable/collections/SortedTableRowCollection.js rename to src/plugins/telemetryTable/collections/TableRowCollection.js index fc44d0d7d4..d84372e9fb 100644 --- a/src/plugins/telemetryTable/collections/SortedTableRowCollection.js +++ b/src/plugins/telemetryTable/collections/TableRowCollection.js @@ -36,85 +36,72 @@ define( /** * @constructor */ - class SortedTableRowCollection extends EventEmitter { + class TableRowCollection extends EventEmitter { constructor() { super(); - this.dupeCheck = false; this.rows = []; + this.columnFilters = {}; + this.addRows = this.addRows.bind(this); + this.removeRowsByObject = this.removeRowsByObject.bind(this); + this.removeRowsByData = this.removeRowsByData.bind(this); - this.add = this.add.bind(this); - this.remove = this.remove.bind(this); + this.clear = this.clear.bind(this); } - /** - * Add a datum or array of data to this telemetry collection - * @fires TelemetryCollection#added - * @param {object | object[]} rows - */ - add(rows) { - if (Array.isArray(rows)) { - this.dupeCheck = false; + removeRowsByObject(keyString) { + let removed = []; - let rowsAdded = rows.filter(this.addOne, this); - if (rowsAdded.length > 0) { - this.emit('add', rowsAdded); - } + this.rows = this.rows.filter((row) => { + if (row.objectKeyString === keyString) { + removed.push(row); - this.dupeCheck = true; - } else { - let wasAdded = this.addOne(rows); - if (wasAdded) { - this.emit('add', rows); + return false; + } else { + return true; } - } + }); + + this.emit('remove', removed); } - /** - * @private - */ - addOne(row) { + addRows(rows, type = 'add') { if (this.sortOptions === undefined) { throw 'Please specify sort options'; } - let isDuplicate = false; + let isFilterTriggeredReset = type === 'filter'; + let anyActiveFilters = Object.keys(this.columnFilters).length > 0; + let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this); - // Going to check for duplicates. Bound the search problem to - // items around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // because the array is guaranteed ordered due to sorted insertion. - let startIx = this.sortedIndex(this.rows, row); - let endIx = undefined; - - if (this.dupeCheck && startIx !== this.rows.length) { - endIx = this.sortedLastIndex(this.rows, row); - - // Create an array of potential dupes, based on having the - // same time stamp - let potentialDupes = this.rows.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, row)); + // if type is filter, then it's a reset of all rows, + // need to wipe current rows + if (isFilterTriggeredReset) { + this.rows = []; } - if (!isDuplicate) { - this.rows.splice(endIx || startIx, 0, row); - - return true; + for (let row of rowsToAdd) { + let index = this.sortedIndex(this.rows, row); + this.rows.splice(index, 0, row); } - return false; + // we emit filter no matter what to trigger + // an update of visible rows + if (rowsToAdd.length > 0 || isFilterTriggeredReset) { + this.emit(type, rowsToAdd); + } } sortedLastIndex(rows, testRow) { return this.sortedIndex(rows, testRow, _.sortedLastIndex); } + /** * Finds the correct insertion point for the given row. * Leverages lodash's `sortedIndex` function which implements a binary search. * @private */ - sortedIndex(rows, testRow, lodashFunction) { + sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) { if (this.rows.length === 0) { return 0; } @@ -123,8 +110,6 @@ define( const firstValue = this.getValueForSortColumn(this.rows[0]); const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]); - lodashFunction = lodashFunction || _.sortedIndexBy; - if (this.sortOptions.direction === 'asc') { if (testRowValue > lastValue) { return this.rows.length; @@ -162,6 +147,22 @@ define( } } + removeRowsByData(data) { + let removed = []; + + this.rows = this.rows.filter((row) => { + if (data.includes(row.fullDatum)) { + removed.push(row); + + return false; + } else { + return true; + } + }); + + this.emit('remove', removed); + } + /** * Sorts the telemetry collection based on the provided sort field * specifier. Subsequent inserts are sorted to maintain specified sport @@ -205,6 +206,7 @@ define( if (arguments.length > 0) { this.sortOptions = sortOptions; this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); + this.emit('sort'); } @@ -212,44 +214,114 @@ define( return Object.assign({}, this.sortOptions); } - removeAllRowsForObject(objectKeyString) { - let removed = []; - this.rows = this.rows.filter(row => { - if (row.objectKeyString === objectKeyString) { - removed.push(row); + setColumnFilter(columnKey, filter) { + filter = filter.trim().toLowerCase(); + let wasBlank = this.columnFilters[columnKey] === undefined; + let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter); + if (filter.length === 0) { + delete this.columnFilters[columnKey]; + } else { + this.columnFilters[columnKey] = filter; + } + + if (isSubset || wasBlank) { + this.rows = this.rows.filter(this.matchesFilters, this); + this.emit('filter'); + } else { + this.emit('resetRowsFromAllData'); + } + + } + + setColumnRegexFilter(columnKey, filter) { + filter = filter.trim(); + this.columnFilters[columnKey] = new RegExp(filter); + + this.emit('resetRowsFromAllData'); + } + + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); + + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; + + return map; + }, {}); + } + + return {}; + } + + // /** + // * @private + // */ + isSubsetOfCurrentFilter(columnKey, filter) { + if (this.columnFilters[columnKey] instanceof RegExp) { + return false; + } + + return this.columnFilters[columnKey] + && filter.startsWith(this.columnFilters[columnKey]) + // startsWith check will otherwise fail when filter cleared + // because anyString.startsWith('') === true + && filter !== ''; + } + + /** + * @private + */ + matchesFilters(row) { + let doesMatchFilters = true; + Object.keys(this.columnFilters).forEach((key) => { + if (!doesMatchFilters || !this.rowHasColumn(row, key)) { return false; } - return true; + let formattedValue = row.getFormattedValue(key); + if (formattedValue === undefined) { + return false; + } + + if (this.columnFilters[key] instanceof RegExp) { + doesMatchFilters = this.columnFilters[key].test(formattedValue); + } else { + doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; + } }); - this.emit('remove', removed); + return doesMatchFilters; } - getValueForSortColumn(row) { - return row.getParsedValue(this.sortOptions.key); - } - - remove(removedRows) { - this.rows = this.rows.filter(row => { - return removedRows.indexOf(row) === -1; - }); - - this.emit('remove', removedRows); + rowHasColumn(row, key) { + return Object.prototype.hasOwnProperty.call(row.columns, key); } getRows() { return this.rows; } + getRowsLength() { + return this.rows.length; + } + + getValueForSortColumn(row) { + return row.getParsedValue(this.sortOptions.key); + } + clear() { let removedRows = this.rows; this.rows = []; this.emit('remove', removedRows); } + + destroy() { + this.removeAllListeners(); + } } - return SortedTableRowCollection; + return TableRowCollection; }); diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue index 07ef0fa5a3..5fd41cbc9f 100644 --- a/src/plugins/telemetryTable/components/table.vue +++ b/src/plugins/telemetryTable/components/table.vue @@ -466,22 +466,21 @@ export default { this.table.on('object-added', this.addObject); this.table.on('object-removed', this.removeObject); - this.table.on('outstanding-requests', this.outstandingRequests); this.table.on('refresh', this.clearRowsAndRerender); this.table.on('historical-rows-processed', this.checkForMarkedRows); + this.table.on('outstanding-requests', this.outstandingRequests); - this.table.filteredRows.on('add', this.rowsAdded); - this.table.filteredRows.on('remove', this.rowsRemoved); - this.table.filteredRows.on('sort', this.updateVisibleRows); - this.table.filteredRows.on('filter', this.updateVisibleRows); + this.table.tableRows.on('add', this.rowsAdded); + this.table.tableRows.on('remove', this.rowsRemoved); + this.table.tableRows.on('sort', this.updateVisibleRows); + this.table.tableRows.on('filter', this.updateVisibleRows); //Default sort - this.sortOptions = this.table.filteredRows.sortBy(); + this.sortOptions = this.table.tableRows.sortBy(); this.scrollable = this.$el.querySelector('.js-telemetry-table__body-w'); this.contentTable = this.$el.querySelector('.js-telemetry-table__content'); this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing'); this.headersHolderEl = this.$el.querySelector('.js-table__headers-w'); - this.table.configuration.on('change', this.updateConfiguration); this.calculateTableSize(); @@ -493,13 +492,14 @@ export default { destroyed() { this.table.off('object-added', this.addObject); this.table.off('object-removed', this.removeObject); - this.table.off('outstanding-requests', this.outstandingRequests); + this.table.off('historical-rows-processed', this.checkForMarkedRows); this.table.off('refresh', this.clearRowsAndRerender); + this.table.off('outstanding-requests', this.outstandingRequests); - this.table.filteredRows.off('add', this.rowsAdded); - this.table.filteredRows.off('remove', this.rowsRemoved); - this.table.filteredRows.off('sort', this.updateVisibleRows); - this.table.filteredRows.off('filter', this.updateVisibleRows); + this.table.tableRows.off('add', this.rowsAdded); + this.table.tableRows.off('remove', this.rowsRemoved); + this.table.tableRows.off('sort', this.updateVisibleRows); + this.table.tableRows.off('filter', this.updateVisibleRows); this.table.configuration.off('change', this.updateConfiguration); @@ -517,13 +517,13 @@ export default { let start = 0; let end = VISIBLE_ROW_COUNT; - let filteredRows = this.table.filteredRows.getRows(); - let filteredRowsLength = filteredRows.length; + let tableRows = this.table.tableRows.getRows(); + let tableRowsLength = tableRows.length; - this.totalNumberOfRows = filteredRowsLength; + this.totalNumberOfRows = tableRowsLength; - if (filteredRowsLength < VISIBLE_ROW_COUNT) { - end = filteredRowsLength; + if (tableRowsLength < VISIBLE_ROW_COUNT) { + end = tableRowsLength; } else { let firstVisible = this.calculateFirstVisibleRow(); let lastVisible = this.calculateLastVisibleRow(); @@ -535,15 +535,15 @@ export default { if (start < 0) { start = 0; - end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength); - } else if (end >= filteredRowsLength) { - end = filteredRowsLength; + end = Math.min(VISIBLE_ROW_COUNT, tableRowsLength); + } else if (end >= tableRowsLength) { + end = tableRowsLength; start = end - VISIBLE_ROW_COUNT + 1; } } this.rowOffset = start; - this.visibleRows = filteredRows.slice(start, end); + this.visibleRows = tableRows.slice(start, end); this.updatingView = false; }); @@ -630,19 +630,19 @@ export default { filterChanged(columnKey) { if (this.enableRegexSearch[columnKey]) { if (this.isCompleteRegex(this.filters[columnKey])) { - this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1)); + this.table.tableRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1)); } else { return; } } else { - this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); + this.table.tableRows.setColumnFilter(columnKey, this.filters[columnKey]); } this.setHeight(); }, clearFilter(columnKey) { this.filters[columnKey] = ''; - this.table.filteredRows.setColumnFilter(columnKey, ''); + this.table.tableRows.setColumnFilter(columnKey, ''); this.setHeight(); }, rowsAdded(rows) { @@ -674,8 +674,8 @@ export default { * Calculates height based on total number of rows, and sets table height. */ setHeight() { - let filteredRowsLength = this.table.filteredRows.getRows().length; - this.totalHeight = this.rowHeight * filteredRowsLength - 1; + let tableRowsLength = this.table.tableRows.getRowsLength(); + this.totalHeight = this.rowHeight * tableRowsLength - 1; // Set element height directly to avoid having to wait for Vue to update DOM // which causes subsequent scroll to use an out of date height. this.contentTable.style.height = this.totalHeight + 'px'; @@ -689,13 +689,13 @@ export default { }); }, exportAllDataAsCSV() { - const justTheData = this.table.filteredRows.getRows() + const justTheData = this.table.tableRows.getRows() .map(row => row.getFormattedDatum(this.headers)); this.exportAsCSV(justTheData); }, exportMarkedDataAsCSV() { - const data = this.table.filteredRows.getRows() + const data = this.table.tableRows.getRows() .filter(row => row.marked === true) .map(row => row.getFormattedDatum(this.headers)); @@ -900,7 +900,7 @@ export default { let lastRowToBeMarked = this.visibleRows[rowIndex]; - let allRows = this.table.filteredRows.getRows(); + let allRows = this.table.tableRows.getRows(); let firstRowIndex = allRows.indexOf(this.markedRows[0]); let lastRowIndex = allRows.indexOf(lastRowToBeMarked); @@ -923,17 +923,17 @@ export default { }, checkForMarkedRows() { this.isShowingMarkedRowsOnly = false; - this.markedRows = this.table.filteredRows.getRows().filter(row => row.marked); + this.markedRows = this.table.tableRows.getRows().filter(row => row.marked); }, showRows(rows) { - this.table.filteredRows.rows = rows; - this.table.filteredRows.emit('filter'); + this.table.tableRows.rows = rows; + this.table.emit('filter'); }, toggleMarkedRows(flag) { if (flag) { this.isShowingMarkedRowsOnly = true; this.userScroll = this.scrollable.scrollTop; - this.allRows = this.table.filteredRows.getRows(); + this.allRows = this.table.tableRows.getRows(); this.showRows(this.markedRows); this.setHeight(); diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index 52b4c84b35..4229ea3ac3 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -48,6 +48,8 @@ describe("the plugin", () => { let tablePlugin; let element; let child; + let historicalProvider; + let originalRouterPath; let unlistenConfigMutation; beforeEach((done) => { @@ -58,7 +60,12 @@ describe("the plugin", () => { tablePlugin = new TablePlugin(); openmct.install(tablePlugin); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + historicalProvider = { + request: () => { + return Promise.resolve([]); + } + }; + spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); element = document.createElement('div'); child = document.createElement('div'); @@ -78,6 +85,8 @@ describe("the plugin", () => { callBack(); }); + originalRouterPath = openmct.router.path; + openmct.on('start', done); openmct.startHeadless(); }); @@ -190,11 +199,12 @@ describe("the plugin", () => { let telemetryPromise = new Promise((resolve) => { telemetryPromiseResolve = resolve; }); - openmct.telemetry.request.and.callFake(() => { + + historicalProvider.request = () => { telemetryPromiseResolve(testTelemetry); return telemetryPromise; - }); + }; openmct.router.path = [testTelemetryObject]; @@ -208,6 +218,10 @@ describe("the plugin", () => { return telemetryPromise.then(() => Vue.nextTick()); }); + afterEach(() => { + openmct.router.path = originalRouterPath; + }); + it("Renders a row for every telemetry datum returned", () => { let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); expect(rows.length).toBe(3); @@ -256,14 +270,14 @@ describe("the plugin", () => { }); it("Supports filtering telemetry by regular text search", () => { - tableInstance.filteredRows.setColumnFilter("some-key", "1"); + tableInstance.tableRows.setColumnFilter("some-key", "1"); return Vue.nextTick().then(() => { let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); expect(filteredRowElements.length).toEqual(1); - tableInstance.filteredRows.setColumnFilter("some-key", ""); + tableInstance.tableRows.setColumnFilter("some-key", ""); return Vue.nextTick().then(() => { let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); @@ -274,14 +288,14 @@ describe("the plugin", () => { }); it("Supports filtering using Regex", () => { - tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$"); + tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$"); return Vue.nextTick().then(() => { let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); expect(filteredRowElements.length).toEqual(0); - tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value"); + tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value"); return Vue.nextTick().then(() => { let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');