[Telemetry Collections] Add Telemetry Collection Functionality to Telemetry API (#3689)

Adds telemetry collections to the telemetry API

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Jamie V 2021-08-10 10:36:33 -07:00 committed by GitHub
parent 2564e75fc9
commit f3fc991a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1252 additions and 1004 deletions

View File

@ -63,7 +63,7 @@ define([
StateGeneratorProvider.prototype.request = function (domainObject, options) { StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start; 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; var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) { if (options.strategy === 'latest' || options.size === 1) {
start = end; start = end;

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([ define([
'../../plugins/displayLayout/CustomStringFormatter', '../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager', './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. * Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters * The `options` argument allows you to specify filters

File diff suppressed because it is too large Load Diff

View 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);
}
}

View File

@ -23,20 +23,18 @@
define([ define([
'EventEmitter', 'EventEmitter',
'lodash', 'lodash',
'./collections/BoundedTableRowCollection', './collections/TableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./TelemetryTableRow', './TelemetryTableRow',
'./TelemetryTableNameColumn',
'./TelemetryTableColumn', './TelemetryTableColumn',
'./TelemetryTableUnitColumn', './TelemetryTableUnitColumn',
'./TelemetryTableConfiguration' './TelemetryTableConfiguration'
], function ( ], function (
EventEmitter, EventEmitter,
_, _,
BoundedTableRowCollection, TableRowCollection,
FilteredTableRowCollection,
TelemetryTableNameColumn,
TelemetryTableRow, TelemetryTableRow,
TelemetryTableNameColumn,
TelemetryTableColumn, TelemetryTableColumn,
TelemetryTableUnitColumn, TelemetryTableUnitColumn,
TelemetryTableConfiguration TelemetryTableConfiguration
@ -48,20 +46,23 @@ define([
this.domainObject = domainObject; this.domainObject = domainObject;
this.openmct = openmct; this.openmct = openmct;
this.rowCount = 100; this.rowCount = 100;
this.subscriptions = {};
this.tableComposition = undefined; this.tableComposition = undefined;
this.telemetryObjects = [];
this.datumCache = []; this.datumCache = [];
this.outstandingRequests = 0;
this.configuration = new TelemetryTableConfiguration(domainObject, openmct); this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.paused = false; this.paused = false;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); 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.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.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.isTelemetryObject = this.isTelemetryObject.bind(this);
this.refreshData = this.refreshData.bind(this); this.refreshData = this.refreshData.bind(this);
this.requestDataFor = this.requestDataFor.bind(this);
this.updateFilters = this.updateFilters.bind(this); this.updateFilters = this.updateFilters.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
@ -102,8 +103,7 @@ define([
} }
createTableRowCollections() { createTableRowCollections() {
this.boundedRows = new BoundedTableRowCollection(this.openmct); this.tableRows = new TableRowCollection();
this.filteredRows = new FilteredTableRowCollection(this.boundedRows);
//Fetch any persisted default sort //Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions; let sortOptions = this.configuration.getConfiguration().sortOptions;
@ -113,11 +113,14 @@ define([
key: this.openmct.time.timeSystem().key, key: this.openmct.time.timeSystem().key,
direction: 'asc' direction: 'asc'
}; };
this.filteredRows.sortBy(sortOptions);
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
} }
loadComposition() { loadComposition() {
this.tableComposition = this.openmct.composition.get(this.domainObject); this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined) { if (this.tableComposition !== undefined) {
this.tableComposition.load().then((composition) => { this.tableComposition.load().then((composition) => {
@ -132,66 +135,64 @@ define([
addTelemetryObject(telemetryObject) { addTelemetryObject(telemetryObject) {
this.addColumnsForObject(telemetryObject, true); this.addColumnsForObject(telemetryObject, true);
this.requestDataFor(telemetryObject);
this.subscribeTo(telemetryObject); const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects.push(telemetryObject); 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); this.emit('object-added', telemetryObject);
} }
updateFilters(updatedFilters) { getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); 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)) { let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.filters = deepCopiedFilters;
this.clearAndResubscribe(); if (this.paused) {
} else { this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
this.filters = deepCopiedFilters; } else {
} this.tableRows.addRows(telemetryRows, 'add');
}
};
} }
clearAndResubscribe() { getTelemetryRemover() {
this.filteredRows.clear(); return (telemetry) => {
this.boundedRows.clear(); if (this.paused) {
Object.keys(this.subscriptions).forEach(this.unsubscribe, this); this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
} else {
this.telemetryObjects.forEach(this.requestDataFor.bind(this)); this.tableRows.removeRowsByData(telemetry);
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);
} }
/** /**
@ -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) { refreshData(bounds, isTick) {
if (!isTick && this.outstandingRequests === 0) { if (!isTick && this.tableRows.outstandingRequests === 0) {
this.filteredRows.clear(); this.tableRows.clear();
this.boundedRows.clear(); this.tableRows.sortBy({
this.boundedRows.sortByTimeSystem(this.openmct.time.timeSystem()); key: this.openmct.time.timeSystem().key,
this.telemetryObjects.forEach(this.requestDataFor); direction: 'asc'
});
this.tableRows.resubscribe();
} }
} }
clearData() { clearData() {
this.filteredRows.clear(); this.tableRows.clear();
this.boundedRows.clear();
this.emit('refresh'); 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) { addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
@ -264,54 +302,18 @@ define([
}); });
} }
createColumn(metadatum) { getColumnMapForObject(objectKeyString) {
return new TelemetryTableColumn(this.openmct, metadatum); let columns = this.configuration.getColumns();
}
createUnitColumn(metadatum) { if (columns[objectKeyString]) {
return new TelemetryTableUnitColumn(this.openmct, metadatum); return columns[objectKeyString].reduce((map, column) => {
} map[column.getKey()] = column;
subscribeTo(telemetryObject) { return map;
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);
this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => { return {};
//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');
} }
buildOptionsFromConfiguration(telemetryObject) { buildOptionsFromConfiguration(telemetryObject) {
@ -323,13 +325,20 @@ define([
return {filters} || {}; return {filters} || {};
} }
unsubscribe(keyString) { createColumn(metadatum) {
this.subscriptions[keyString](); return new TelemetryTableColumn(this.openmct, metadatum);
delete this.subscriptions[keyString]; }
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
isTelemetryObject(domainObject) {
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
} }
sortBy(sortOptions) { sortBy(sortOptions) {
this.filteredRows.sortBy(sortOptions); this.tableRows.sortBy(sortOptions);
if (this.openmct.editor.isEditing()) { if (this.openmct.editor.isEditing()) {
let configuration = this.configuration.getConfiguration(); 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() { pause() {
this.paused = true; this.paused = true;
this.boundedRows.unsubscribeFromBounds();
} }
unpause() { unpause() {
this.paused = false; this.paused = false;
this.processDatumCache(); this.runDelayedActions();
this.boundedRows.subscribeToBounds();
} }
destroy() { destroy() {
this.boundedRows.destroy(); this.tableRows.destroy();
this.filteredRows.destroy();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this); 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('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.refreshData); this.openmct.time.off('timeSystem', this.refreshData);

View File

@ -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;
});

View File

@ -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;
});

View File

@ -36,85 +36,72 @@ define(
/** /**
* @constructor * @constructor
*/ */
class SortedTableRowCollection extends EventEmitter { class TableRowCollection extends EventEmitter {
constructor() { constructor() {
super(); super();
this.dupeCheck = false;
this.rows = []; 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.clear = this.clear.bind(this);
this.remove = this.remove.bind(this);
} }
/** removeRowsByObject(keyString) {
* Add a datum or array of data to this telemetry collection let removed = [];
* @fires TelemetryCollection#added
* @param {object | object[]} rows
*/
add(rows) {
if (Array.isArray(rows)) {
this.dupeCheck = false;
let rowsAdded = rows.filter(this.addOne, this); this.rows = this.rows.filter((row) => {
if (rowsAdded.length > 0) { if (row.objectKeyString === keyString) {
this.emit('add', rowsAdded); removed.push(row);
}
this.dupeCheck = true; return false;
} else { } else {
let wasAdded = this.addOne(rows); return true;
if (wasAdded) {
this.emit('add', rows);
} }
} });
this.emit('remove', removed);
} }
/** addRows(rows, type = 'add') {
* @private
*/
addOne(row) {
if (this.sortOptions === undefined) { if (this.sortOptions === undefined) {
throw 'Please specify sort options'; 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 // if type is filter, then it's a reset of all rows,
// items around the given time. Use sortedIndex because it // need to wipe current rows
// employs a binary search which is O(log n). Can use binary search if (isFilterTriggeredReset) {
// because the array is guaranteed ordered due to sorted insertion. this.rows = [];
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 (!isDuplicate) { for (let row of rowsToAdd) {
this.rows.splice(endIx || startIx, 0, row); let index = this.sortedIndex(this.rows, row);
this.rows.splice(index, 0, row);
return true;
} }
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) { sortedLastIndex(rows, testRow) {
return this.sortedIndex(rows, testRow, _.sortedLastIndex); return this.sortedIndex(rows, testRow, _.sortedLastIndex);
} }
/** /**
* Finds the correct insertion point for the given row. * Finds the correct insertion point for the given row.
* Leverages lodash's `sortedIndex` function which implements a binary search. * Leverages lodash's `sortedIndex` function which implements a binary search.
* @private * @private
*/ */
sortedIndex(rows, testRow, lodashFunction) { sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
if (this.rows.length === 0) { if (this.rows.length === 0) {
return 0; return 0;
} }
@ -123,8 +110,6 @@ define(
const firstValue = this.getValueForSortColumn(this.rows[0]); const firstValue = this.getValueForSortColumn(this.rows[0]);
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]); const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
lodashFunction = lodashFunction || _.sortedIndexBy;
if (this.sortOptions.direction === 'asc') { if (this.sortOptions.direction === 'asc') {
if (testRowValue > lastValue) { if (testRowValue > lastValue) {
return this.rows.length; 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 * Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport * specifier. Subsequent inserts are sorted to maintain specified sport
@ -205,6 +206,7 @@ define(
if (arguments.length > 0) { if (arguments.length > 0) {
this.sortOptions = sortOptions; this.sortOptions = sortOptions;
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
this.emit('sort'); this.emit('sort');
} }
@ -212,44 +214,114 @@ define(
return Object.assign({}, this.sortOptions); return Object.assign({}, this.sortOptions);
} }
removeAllRowsForObject(objectKeyString) { setColumnFilter(columnKey, filter) {
let removed = []; filter = filter.trim().toLowerCase();
this.rows = this.rows.filter(row => { let wasBlank = this.columnFilters[columnKey] === undefined;
if (row.objectKeyString === objectKeyString) { let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);
removed.push(row);
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 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) { rowHasColumn(row, key) {
return row.getParsedValue(this.sortOptions.key); return Object.prototype.hasOwnProperty.call(row.columns, key);
}
remove(removedRows) {
this.rows = this.rows.filter(row => {
return removedRows.indexOf(row) === -1;
});
this.emit('remove', removedRows);
} }
getRows() { getRows() {
return this.rows; return this.rows;
} }
getRowsLength() {
return this.rows.length;
}
getValueForSortColumn(row) {
return row.getParsedValue(this.sortOptions.key);
}
clear() { clear() {
let removedRows = this.rows; let removedRows = this.rows;
this.rows = []; this.rows = [];
this.emit('remove', removedRows); this.emit('remove', removedRows);
} }
destroy() {
this.removeAllListeners();
}
} }
return SortedTableRowCollection; return TableRowCollection;
}); });

View File

@ -466,22 +466,21 @@ export default {
this.table.on('object-added', this.addObject); this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject); this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.on('refresh', this.clearRowsAndRerender); this.table.on('refresh', this.clearRowsAndRerender);
this.table.on('historical-rows-processed', this.checkForMarkedRows); this.table.on('historical-rows-processed', this.checkForMarkedRows);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.filteredRows.on('add', this.rowsAdded); this.table.tableRows.on('add', this.rowsAdded);
this.table.filteredRows.on('remove', this.rowsRemoved); this.table.tableRows.on('remove', this.rowsRemoved);
this.table.filteredRows.on('sort', this.updateVisibleRows); this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.filteredRows.on('filter', this.updateVisibleRows); this.table.tableRows.on('filter', this.updateVisibleRows);
//Default sort //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.scrollable = this.$el.querySelector('.js-telemetry-table__body-w');
this.contentTable = this.$el.querySelector('.js-telemetry-table__content'); this.contentTable = this.$el.querySelector('.js-telemetry-table__content');
this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing'); this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing');
this.headersHolderEl = this.$el.querySelector('.js-table__headers-w'); this.headersHolderEl = this.$el.querySelector('.js-table__headers-w');
this.table.configuration.on('change', this.updateConfiguration); this.table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize(); this.calculateTableSize();
@ -493,13 +492,14 @@ export default {
destroyed() { destroyed() {
this.table.off('object-added', this.addObject); this.table.off('object-added', this.addObject);
this.table.off('object-removed', this.removeObject); 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('refresh', this.clearRowsAndRerender);
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.filteredRows.off('add', this.rowsAdded); this.table.tableRows.off('add', this.rowsAdded);
this.table.filteredRows.off('remove', this.rowsRemoved); this.table.tableRows.off('remove', this.rowsRemoved);
this.table.filteredRows.off('sort', this.updateVisibleRows); this.table.tableRows.off('sort', this.updateVisibleRows);
this.table.filteredRows.off('filter', this.updateVisibleRows); this.table.tableRows.off('filter', this.updateVisibleRows);
this.table.configuration.off('change', this.updateConfiguration); this.table.configuration.off('change', this.updateConfiguration);
@ -517,13 +517,13 @@ export default {
let start = 0; let start = 0;
let end = VISIBLE_ROW_COUNT; let end = VISIBLE_ROW_COUNT;
let filteredRows = this.table.filteredRows.getRows(); let tableRows = this.table.tableRows.getRows();
let filteredRowsLength = filteredRows.length; let tableRowsLength = tableRows.length;
this.totalNumberOfRows = filteredRowsLength; this.totalNumberOfRows = tableRowsLength;
if (filteredRowsLength < VISIBLE_ROW_COUNT) { if (tableRowsLength < VISIBLE_ROW_COUNT) {
end = filteredRowsLength; end = tableRowsLength;
} else { } else {
let firstVisible = this.calculateFirstVisibleRow(); let firstVisible = this.calculateFirstVisibleRow();
let lastVisible = this.calculateLastVisibleRow(); let lastVisible = this.calculateLastVisibleRow();
@ -535,15 +535,15 @@ export default {
if (start < 0) { if (start < 0) {
start = 0; start = 0;
end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength); end = Math.min(VISIBLE_ROW_COUNT, tableRowsLength);
} else if (end >= filteredRowsLength) { } else if (end >= tableRowsLength) {
end = filteredRowsLength; end = tableRowsLength;
start = end - VISIBLE_ROW_COUNT + 1; start = end - VISIBLE_ROW_COUNT + 1;
} }
} }
this.rowOffset = start; this.rowOffset = start;
this.visibleRows = filteredRows.slice(start, end); this.visibleRows = tableRows.slice(start, end);
this.updatingView = false; this.updatingView = false;
}); });
@ -630,19 +630,19 @@ export default {
filterChanged(columnKey) { filterChanged(columnKey) {
if (this.enableRegexSearch[columnKey]) { if (this.enableRegexSearch[columnKey]) {
if (this.isCompleteRegex(this.filters[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 { } else {
return; return;
} }
} else { } else {
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); this.table.tableRows.setColumnFilter(columnKey, this.filters[columnKey]);
} }
this.setHeight(); this.setHeight();
}, },
clearFilter(columnKey) { clearFilter(columnKey) {
this.filters[columnKey] = ''; this.filters[columnKey] = '';
this.table.filteredRows.setColumnFilter(columnKey, ''); this.table.tableRows.setColumnFilter(columnKey, '');
this.setHeight(); this.setHeight();
}, },
rowsAdded(rows) { rowsAdded(rows) {
@ -674,8 +674,8 @@ export default {
* Calculates height based on total number of rows, and sets table height. * Calculates height based on total number of rows, and sets table height.
*/ */
setHeight() { setHeight() {
let filteredRowsLength = this.table.filteredRows.getRows().length; let tableRowsLength = this.table.tableRows.getRowsLength();
this.totalHeight = this.rowHeight * filteredRowsLength - 1; this.totalHeight = this.rowHeight * tableRowsLength - 1;
// Set element height directly to avoid having to wait for Vue to update DOM // 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. // which causes subsequent scroll to use an out of date height.
this.contentTable.style.height = this.totalHeight + 'px'; this.contentTable.style.height = this.totalHeight + 'px';
@ -689,13 +689,13 @@ export default {
}); });
}, },
exportAllDataAsCSV() { exportAllDataAsCSV() {
const justTheData = this.table.filteredRows.getRows() const justTheData = this.table.tableRows.getRows()
.map(row => row.getFormattedDatum(this.headers)); .map(row => row.getFormattedDatum(this.headers));
this.exportAsCSV(justTheData); this.exportAsCSV(justTheData);
}, },
exportMarkedDataAsCSV() { exportMarkedDataAsCSV() {
const data = this.table.filteredRows.getRows() const data = this.table.tableRows.getRows()
.filter(row => row.marked === true) .filter(row => row.marked === true)
.map(row => row.getFormattedDatum(this.headers)); .map(row => row.getFormattedDatum(this.headers));
@ -900,7 +900,7 @@ export default {
let lastRowToBeMarked = this.visibleRows[rowIndex]; 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 firstRowIndex = allRows.indexOf(this.markedRows[0]);
let lastRowIndex = allRows.indexOf(lastRowToBeMarked); let lastRowIndex = allRows.indexOf(lastRowToBeMarked);
@ -923,17 +923,17 @@ export default {
}, },
checkForMarkedRows() { checkForMarkedRows() {
this.isShowingMarkedRowsOnly = false; 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) { showRows(rows) {
this.table.filteredRows.rows = rows; this.table.tableRows.rows = rows;
this.table.filteredRows.emit('filter'); this.table.emit('filter');
}, },
toggleMarkedRows(flag) { toggleMarkedRows(flag) {
if (flag) { if (flag) {
this.isShowingMarkedRowsOnly = true; this.isShowingMarkedRowsOnly = true;
this.userScroll = this.scrollable.scrollTop; this.userScroll = this.scrollable.scrollTop;
this.allRows = this.table.filteredRows.getRows(); this.allRows = this.table.tableRows.getRows();
this.showRows(this.markedRows); this.showRows(this.markedRows);
this.setHeight(); this.setHeight();

View File

@ -48,6 +48,8 @@ describe("the plugin", () => {
let tablePlugin; let tablePlugin;
let element; let element;
let child; let child;
let historicalProvider;
let originalRouterPath;
let unlistenConfigMutation; let unlistenConfigMutation;
beforeEach((done) => { beforeEach((done) => {
@ -58,7 +60,12 @@ describe("the plugin", () => {
tablePlugin = new TablePlugin(); tablePlugin = new TablePlugin();
openmct.install(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'); element = document.createElement('div');
child = document.createElement('div'); child = document.createElement('div');
@ -78,6 +85,8 @@ describe("the plugin", () => {
callBack(); callBack();
}); });
originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });
@ -190,11 +199,12 @@ describe("the plugin", () => {
let telemetryPromise = new Promise((resolve) => { let telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve; telemetryPromiseResolve = resolve;
}); });
openmct.telemetry.request.and.callFake(() => {
historicalProvider.request = () => {
telemetryPromiseResolve(testTelemetry); telemetryPromiseResolve(testTelemetry);
return telemetryPromise; return telemetryPromise;
}); };
openmct.router.path = [testTelemetryObject]; openmct.router.path = [testTelemetryObject];
@ -208,6 +218,10 @@ describe("the plugin", () => {
return telemetryPromise.then(() => Vue.nextTick()); return telemetryPromise.then(() => Vue.nextTick());
}); });
afterEach(() => {
openmct.router.path = originalRouterPath;
});
it("Renders a row for every telemetry datum returned", () => { it("Renders a row for every telemetry datum returned", () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(rows.length).toBe(3); expect(rows.length).toBe(3);
@ -256,14 +270,14 @@ describe("the plugin", () => {
}); });
it("Supports filtering telemetry by regular text search", () => { it("Supports filtering telemetry by regular text search", () => {
tableInstance.filteredRows.setColumnFilter("some-key", "1"); tableInstance.tableRows.setColumnFilter("some-key", "1");
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(1); expect(filteredRowElements.length).toEqual(1);
tableInstance.filteredRows.setColumnFilter("some-key", ""); tableInstance.tableRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
@ -274,14 +288,14 @@ describe("the plugin", () => {
}); });
it("Supports filtering using Regex", () => { it("Supports filtering using Regex", () => {
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$"); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0); expect(filteredRowElements.length).toEqual(0);
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value"); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');