Compare commits
	
		
			98 Commits
		
	
	
		
			release/1.
			...
			telemetry-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b46441ee77 | ||
| 
						 | 
					3ab120ee26 | ||
| 
						 | 
					a7a9740610 | ||
| 
						 | 
					b0fb8bfb03 | ||
| 
						 | 
					3865d95a5b | ||
| 
						 | 
					4d1fc642ea | ||
| 
						 | 
					5d5f13af79 | ||
| 
						 | 
					b8e8a70211 | ||
| 
						 | 
					0ab4dbbdb2 | ||
| 
						 | 
					98b77f46a0 | ||
| 
						 | 
					25d83a2599 | ||
| 
						 | 
					560a2ce95a | ||
| 
						 | 
					a735a5b680 | ||
| 
						 | 
					2feb1cac11 | ||
| 
						 | 
					2d64f0e816 | ||
| 
						 | 
					2c6c069e6c | ||
| 
						 | 
					3fca0fbf72 | ||
| 
						 | 
					7fbe22ab10 | ||
| 
						 | 
					08f924825b | ||
| 
						 | 
					073b7f28dd | ||
| 
						 | 
					20fe7a25eb | ||
| 
						 | 
					1309d73a12 | ||
| 
						 | 
					94a9dfc498 | ||
| 
						 | 
					d8c0d4b656 | ||
| 
						 | 
					23988f5afe | ||
| 
						 | 
					53e0d8131e | ||
| 
						 | 
					c153167045 | ||
| 
						 | 
					be6ab0ee15 | ||
| 
						 | 
					c5fe38dd70 | ||
| 
						 | 
					7c4d70cfda | ||
| 
						 | 
					2ae3b1678e | ||
| 
						 | 
					5208bdf891 | ||
| 
						 | 
					4dc2069476 | ||
| 
						 | 
					2d4a3a3a5d | ||
| 
						 | 
					bbbb176e96 | ||
| 
						 | 
					30d9012af9 | ||
| 
						 | 
					1a02da38d2 | ||
| 
						 | 
					3a585f8773 | ||
| 
						 | 
					b17d92d2ef | ||
| 
						 | 
					0f159f7ce1 | ||
| 
						 | 
					f2140c768c | ||
| 
						 | 
					4bdb8cf0ea | ||
| 
						 | 
					33fb70442a | ||
| 
						 | 
					993aa78562 | ||
| 
						 | 
					b97a0616d6 | ||
| 
						 | 
					d76cb9d344 | ||
| 
						 | 
					b4cfbd8d27 | ||
| 
						 | 
					defbfe6b62 | ||
| 
						 | 
					d4d39a4135 | ||
| 
						 | 
					155cbb95fb | ||
| 
						 | 
					131ef579f9 | ||
| 
						 | 
					f87b87c248 | ||
| 
						 | 
					bdc50564f2 | ||
| 
						 | 
					d349315944 | ||
| 
						 | 
					c5034beb9c | ||
| 
						 | 
					5b97d96b2e | ||
| 
						 | 
					6a586635c3 | ||
| 
						 | 
					fc8021c72c | ||
| 
						 | 
					7fe19d424f | ||
| 
						 | 
					515c18278b | ||
| 
						 | 
					9b8518e6d2 | ||
| 
						 | 
					4639a1eabc | ||
| 
						 | 
					26f6407283 | ||
| 
						 | 
					b9fc3d0499 | ||
| 
						 | 
					5db71db974 | ||
| 
						 | 
					0b9bdb0c74 | ||
| 
						 | 
					cdedea4bed | ||
| 
						 | 
					82096ea887 | ||
| 
						 | 
					43488646c0 | ||
| 
						 | 
					71bbb67d78 | ||
| 
						 | 
					ad96e5ce66 | ||
| 
						 | 
					1dc245c9ee | ||
| 
						 | 
					c6c432005f | ||
| 
						 | 
					8a1a2bbf84 | ||
| 
						 | 
					0cbb0e7ae2 | ||
| 
						 | 
					993576a471 | ||
| 
						 | 
					d69056361f | ||
| 
						 | 
					30da83eb0a | ||
| 
						 | 
					e2b4f0a3fa | ||
| 
						 | 
					6f952789ed | ||
| 
						 | 
					2bf06dbf23 | ||
| 
						 | 
					b02bd08a12 | ||
| 
						 | 
					a9b70c6f9a | ||
| 
						 | 
					c09fc6110b | ||
| 
						 | 
					84e45e2543 | ||
| 
						 | 
					8f50425043 | ||
| 
						 | 
					f7f1b0d97e | ||
| 
						 | 
					cb08a82a5d | ||
| 
						 | 
					d9caa734d3 | ||
| 
						 | 
					72848849dd | ||
| 
						 | 
					66130ba542 | ||
| 
						 | 
					895bdc164f | ||
| 
						 | 
					c9728144a5 | ||
| 
						 | 
					76fec7f3bc | ||
| 
						 | 
					1b4717065a | ||
| 
						 | 
					e24542c1a6 | ||
| 
						 | 
					b08f3106ed | ||
| 
						 | 
					700bc7616d | 
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										366
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    });
 | 
			
		||||
@@ -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;
 | 
			
		||||
    });
 | 
			
		||||
@@ -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;
 | 
			
		||||
    });
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user