mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
[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:
parent
2564e75fc9
commit
f3fc991a74
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user