Compare commits

...

6 Commits

Author SHA1 Message Date
9a5f8ce82f Merge branch 'release/2.0.5' into plots-fix-initialize 2022-07-07 19:38:04 -07:00
063df721ae [Remote Clock] Wait for first tick and recalculate historical request bounds (#5433)
* Updated to ES6 class
* added request intercept functionality to telemetry api, added a request interceptor for remote clock
* add remoteClock e2e test stub

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 23:51:12 +00:00
a09db30b32 Allow endpoints with a single enum metadata value in Bar/Line graphs (#5443)
* If there is only 1 metadata value, set yKey to none. Also, fix bug for determining the name of a metadata value
* Update tests for enum metadata values

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 16:44:09 -07:00
9d89bdd6d3 [Static Root] Static Root Plugin not loading (#5455)
* Log if hitting falsy leafValue

* Add some logging

* Remove logs and specify null/undefined

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-07 15:00:33 -07:00
ed9ca2829b fix pathing (#5452)
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-07 14:33:05 -07:00
4a4d6a1038 Initialize the plot only after the chart is ready 2022-06-29 18:56:41 -07:00
18 changed files with 545 additions and 355 deletions

View File

@ -0,0 +1,41 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
const { test } = require('../../../fixtures.js');
// eslint-disable-next-line no-unused-vars
const { expect } = require('@playwright/test');
test.describe('Remote Clock', () => {
// eslint-disable-next-line require-await
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5221'
});
// addInitScript to with remote clock
// Switch time conductor mode to 'remote clock'
// Navigate to telemetry
// Verify that the plot renders historical data within the correct bounds
// Refresh the page
// Verify again that the plot renders historical data within the correct bounds
});
});

View File

@ -24,7 +24,7 @@ const { test } = require('../../../fixtures');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
test('unpauses when paused by button and user changes bounds', async ({ page }) => { test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113' description: 'https://github.com/nasa/openmct/issues/5113'
@ -71,25 +71,34 @@ test.describe('Telemetry Table', () => {
]); ]);
// Click pause button // Click pause button
const pauseButton = await page.locator('button.c-button.icon-pause'); const pauseButton = page.locator('button.c-button.icon-pause');
await pauseButton.click(); await pauseButton.click();
const tableWrapper = await page.locator('div.c-table-wrapper'); const tableWrapper = page.locator('div.c-table-wrapper');
await expect(tableWrapper).toHaveClass(/is-paused/); await expect(tableWrapper).toHaveClass(/is-paused/);
// Arbitrarily change end date to some time in the future // Subtract 5 minutes from the current end bound datetime and set it
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click(); await endTimeInput.click();
let endDate = await endTimeInput.inputValue(); let endDate = await endTimeInput.inputValue();
endDate = new Date(endDate); endDate = new Date(endDate);
endDate.setUTCDate(endDate.getUTCDate() + 1);
endDate = endDate.toISOString().replace(/T.*/, ''); endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' ');
await endTimeInput.fill(''); await endTimeInput.fill('');
await endTimeInput.fill(endDate); await endTimeInput.fill(endDate);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await expect(tableWrapper).not.toHaveClass(/is-paused/); await expect(tableWrapper).not.toHaveClass(/is-paused/);
// Get the most recent telemetry date
const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
// Verify that it is <= our new end bound
const latestMilliseconds = Date.parse(latestTelemetryDate);
const endBoundMilliseconds = Date.parse(endDate);
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
}); });
}); });

View File

@ -56,7 +56,7 @@ test.beforeEach(async ({ context }) => {
test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => { test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addRestrictedNotebook.js') }); await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') });
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button //Click the Create button

View File

@ -203,7 +203,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name telemetry * @name telemetry
*/ */
this.telemetry = new api.TelemetryAPI(this); this.telemetry = new api.TelemetryAPI.default(this);
/** /**
* An interface for creating new indicators and changing them dynamically. * An interface for creating new indicators and changing them dynamically.

View File

@ -20,122 +20,18 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection"); import TelemetryCollection from './TelemetryCollection';
import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter';
import TelemetryMetadataManager from './TelemetryMetadataManager';
import TelemetryValueFormatter from './TelemetryValueFormatter';
import DefaultMetadataProvider from './DefaultMetadataProvider';
import objectUtils from 'objectUtils';
import _ from 'lodash';
define([ export default class TelemetryAPI {
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
'./TelemetryValueFormatter',
'./DefaultMetadataProvider',
'objectUtils',
'lodash'
], function (
CustomStringFormatter,
TelemetryMetadataManager,
TelemetryValueFormatter,
DefaultMetadataProvider,
objectUtils,
_
) {
/**
* A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
/** constructor(openmct) {
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/
function TelemetryAPI(openmct) {
this.openmct = openmct; this.openmct = openmct;
this.formatMapCache = new WeakMap(); this.formatMapCache = new WeakMap();
@ -148,12 +44,14 @@ define([
this.requestProviders = []; this.requestProviders = [];
this.subscriptionProviders = []; this.subscriptionProviders = [];
this.valueFormatterCache = new WeakMap(); this.valueFormatterCache = new WeakMap();
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
} }
TelemetryAPI.prototype.abortAllRequests = function () { abortAllRequests() {
this.requestAbortControllers.forEach((controller) => controller.abort()); this.requestAbortControllers.forEach((controller) => controller.abort());
this.requestAbortControllers.clear(); this.requestAbortControllers.clear();
}; }
/** /**
* Return Custom String Formatter * Return Custom String Formatter
@ -162,9 +60,9 @@ define([
* @param {string} format custom formatter string (eg: %.4f, &lts etc.) * @param {string} format custom formatter string (eg: %.4f, &lts etc.)
* @returns {CustomStringFormatter} * @returns {CustomStringFormatter}
*/ */
TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { customStringFormatter(valueMetadata, format) {
return new CustomStringFormatter.default(this.openmct, valueMetadata, format); return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
}; }
/** /**
* Return true if the given domainObject is a telemetry object. A telemetry * Return true if the given domainObject is a telemetry object. A telemetry
@ -174,9 +72,9 @@ define([
* @param {module:openmct.DomainObject} domainObject * @param {module:openmct.DomainObject} domainObject
* @returns {boolean} true if the object is a telemetry object. * @returns {boolean} true if the object is a telemetry object.
*/ */
TelemetryAPI.prototype.isTelemetryObject = function (domainObject) { isTelemetryObject(domainObject) {
return Boolean(this.findMetadataProvider(domainObject)); return Boolean(this.findMetadataProvider(domainObject));
}; }
/** /**
* Check if this provider can supply telemetry data associated with * Check if this provider can supply telemetry data associated with
@ -188,10 +86,10 @@ define([
* @returns {boolean} true if telemetry can be provided * @returns {boolean} true if telemetry can be provided
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { canProvideTelemetry(domainObject) {
return Boolean(this.findSubscriptionProvider(domainObject)) return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject)); || Boolean(this.findRequestProvider(domainObject));
}; }
/** /**
* Register a telemetry provider with the telemetry service. This * Register a telemetry provider with the telemetry service. This
@ -201,7 +99,7 @@ define([
* @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
* telemetry provider * telemetry provider
*/ */
TelemetryAPI.prototype.addProvider = function (provider) { addProvider(provider) {
if (provider.supportsRequest) { if (provider.supportsRequest) {
this.requestProviders.unshift(provider); this.requestProviders.unshift(provider);
} }
@ -217,54 +115,54 @@ define([
if (provider.supportsLimits) { if (provider.supportsLimits) {
this.limitProviders.unshift(provider); this.limitProviders.unshift(provider);
} }
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findSubscriptionProvider = function () { findSubscriptionProvider() {
const args = Array.prototype.slice.apply(arguments); const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) { function supportsDomainObject(provider) {
return provider.supportsSubscribe.apply(provider, args); return provider.supportsSubscribe.apply(provider, args);
} }
return this.subscriptionProviders.filter(supportsDomainObject)[0]; return this.subscriptionProviders.filter(supportsDomainObject)[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findRequestProvider = function (domainObject) { findRequestProvider(domainObject) {
const args = Array.prototype.slice.apply(arguments); const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) { function supportsDomainObject(provider) {
return provider.supportsRequest.apply(provider, args); return provider.supportsRequest.apply(provider, args);
} }
return this.requestProviders.filter(supportsDomainObject)[0]; return this.requestProviders.filter(supportsDomainObject)[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findMetadataProvider = function (domainObject) { findMetadataProvider(domainObject) {
return this.metadataProviders.filter(function (p) { return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject); return p.supportsMetadata(domainObject);
})[0]; })[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) { findLimitEvaluator(domainObject) {
return this.limitProviders.filter(function (p) { return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject); return p.supportsLimits(domainObject);
})[0]; })[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.standardizeRequestOptions = function (options) { standardizeRequestOptions(options) {
if (!Object.prototype.hasOwnProperty.call(options, 'start')) { if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
options.start = this.openmct.time.bounds().start; options.start = this.openmct.time.bounds().start;
} }
@ -276,7 +174,47 @@ define([
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key; options.domain = this.openmct.time.timeSystem().key;
} }
}; }
/**
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
* The request will be modifyed when it is received and will be returned in it's modified state
* The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
* @method addRequestInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addRequestInterceptor(requestInterceptorDef) {
this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);
}
/**
* Retrieve the request interceptors for a given domain object.
* @private
*/
#getInterceptorsForRequest(identifier, request) {
return this.requestInterceptorRegistry.getInterceptors(identifier, request);
}
/**
* Invoke interceptors if applicable for a given domain object.
*/
async applyRequestInterceptors(domainObject, request) {
const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);
if (interceptors.length === 0) {
return request;
}
let modifiedRequest = { ...request };
for (let interceptor of interceptors) {
modifiedRequest = await interceptor.invoke(modifiedRequest);
}
return modifiedRequest;
}
/** /**
* Request telemetry collection for a domain object. * Request telemetry collection for a domain object.
@ -292,13 +230,13 @@ define([
* options for this telemetry collection request * options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance * @returns {TelemetryCollection} a TelemetryCollection instance
*/ */
TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { requestCollection(domainObject, options = {}) {
return new TelemetryCollection( return new TelemetryCollection(
this.openmct, this.openmct,
domainObject, domainObject,
options options
); );
}; }
/** /**
* Request historical telemetry for a domain object. * Request historical telemetry for a domain object.
@ -315,7 +253,7 @@ define([
* @returns {Promise.<object[]>} a promise for an array of * @returns {Promise.<object[]>} a promise for an array of
* telemetry data * telemetry data
*/ */
TelemetryAPI.prototype.request = function (domainObject) { async request(domainObject) {
if (this.noRequestProviderForAllObjects) { if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]); return Promise.resolve([]);
} }
@ -330,6 +268,7 @@ define([
this.requestAbortControllers.add(abortController); this.requestAbortControllers.add(abortController);
this.standardizeRequestOptions(arguments[1]); this.standardizeRequestOptions(arguments[1]);
const provider = this.findRequestProvider.apply(this, arguments); const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) { if (!provider) {
this.requestAbortControllers.delete(abortController); this.requestAbortControllers.delete(abortController);
@ -337,6 +276,8 @@ define([
return this.handleMissingRequestProvider(domainObject); return this.handleMissingRequestProvider(domainObject);
} }
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
return provider.request.apply(provider, arguments) return provider.request.apply(provider, arguments)
.catch((rejected) => { .catch((rejected) => {
if (rejected.name !== 'AbortError') { if (rejected.name !== 'AbortError') {
@ -348,7 +289,7 @@ define([
}).finally(() => { }).finally(() => {
this.requestAbortControllers.delete(abortController); this.requestAbortControllers.delete(abortController);
}); });
}; }
/** /**
* Subscribe to realtime telemetry for a specific domain object. * Subscribe to realtime telemetry for a specific domain object.
@ -364,7 +305,7 @@ define([
* @returns {Function} a function which may be called to terminate * @returns {Function} a function which may be called to terminate
* the subscription * the subscription
*/ */
TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) { subscribe(domainObject, callback, options) {
const provider = this.findSubscriptionProvider(domainObject); const provider = this.findSubscriptionProvider(domainObject);
if (!this.subscribeCache) { if (!this.subscribeCache) {
@ -401,7 +342,7 @@ define([
delete this.subscribeCache[keyString]; delete this.subscribeCache[keyString];
} }
}.bind(this); }.bind(this);
}; }
/** /**
* Get telemetry metadata for a given domain object. Returns a telemetry * Get telemetry metadata for a given domain object. Returns a telemetry
@ -410,7 +351,7 @@ define([
* *
* @returns {TelemetryMetadataManager} * @returns {TelemetryMetadataManager}
*/ */
TelemetryAPI.prototype.getMetadata = function (domainObject) { getMetadata(domainObject) {
if (!this.metadataCache.has(domainObject)) { if (!this.metadataCache.has(domainObject)) {
const metadataProvider = this.findMetadataProvider(domainObject); const metadataProvider = this.findMetadataProvider(domainObject);
if (!metadataProvider) { if (!metadataProvider) {
@ -426,14 +367,14 @@ define([
} }
return this.metadataCache.get(domainObject); return this.metadataCache.get(domainObject);
}; }
/** /**
* Return an array of valueMetadatas that are common to all supplied * Return an array of valueMetadatas that are common to all supplied
* telemetry objects and match the requested hints. * telemetry objects and match the requested hints.
* *
*/ */
TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { commonValuesForHints(metadatas, hints) {
const options = metadatas.map(function (metadata) { const options = metadatas.map(function (metadata) {
const values = metadata.valuesForHints(hints); const values = metadata.valuesForHints(hints);
@ -453,14 +394,14 @@ define([
}); });
return _.sortBy(options, sortKeys); return _.sortBy(options, sortKeys);
}; }
/** /**
* Get a value formatter for a given valueMetadata. * Get a value formatter for a given valueMetadata.
* *
* @returns {TelemetryValueFormatter} * @returns {TelemetryValueFormatter}
*/ */
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { getValueFormatter(valueMetadata) {
if (!this.valueFormatterCache.has(valueMetadata)) { if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set( this.valueFormatterCache.set(
valueMetadata, valueMetadata,
@ -469,7 +410,7 @@ define([
} }
return this.valueFormatterCache.get(valueMetadata); return this.valueFormatterCache.get(valueMetadata);
}; }
/** /**
* Get a value formatter for a given key. * Get a value formatter for a given key.
@ -477,9 +418,9 @@ define([
* *
* @returns {Format} * @returns {Format}
*/ */
TelemetryAPI.prototype.getFormatter = function (key) { getFormatter(key) {
return this.formatters.get(key); return this.formatters.get(key);
}; }
/** /**
* Get a format map of all value formatters for a given piece of telemetry * Get a format map of all value formatters for a given piece of telemetry
@ -487,7 +428,7 @@ define([
* *
* @returns {Object<String, {TelemetryValueFormatter}>} * @returns {Object<String, {TelemetryValueFormatter}>}
*/ */
TelemetryAPI.prototype.getFormatMap = function (metadata) { getFormatMap(metadata) {
if (!metadata) { if (!metadata) {
return {}; return {};
} }
@ -502,14 +443,14 @@ define([
} }
return this.formatMapCache.get(metadata); return this.formatMapCache.get(metadata);
}; }
/** /**
* Error Handling: Missing Request provider * Error Handling: Missing Request provider
* *
* @returns Promise * @returns Promise
*/ */
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { handleMissingRequestProvider(domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
@ -532,15 +473,15 @@ define([
console.warn(detailMessage); console.warn(detailMessage);
return Promise.resolve([]); return Promise.resolve([]);
}; }
/** /**
* Register a new telemetry data formatter. * Register a new telemetry data formatter.
* @param {Format} format the * @param {Format} format the
*/ */
TelemetryAPI.prototype.addFormat = function (format) { addFormat(format) {
this.formatters.set(format.key, format); this.formatters.set(format.key, format);
}; }
/** /**
* Get a limit evaluator for this domain object. * Get a limit evaluator for this domain object.
@ -558,9 +499,9 @@ define([
* @method limitEvaluator * @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.limitEvaluator = function (domainObject) { limitEvaluator(domainObject) {
return this.getLimitEvaluator(domainObject); return this.getLimitEvaluator(domainObject);
}; }
/** /**
* Get a limits for this domain object. * Get a limits for this domain object.
@ -578,9 +519,9 @@ define([
* @method limits * @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.limitDefinition = function (domainObject) { limitDefinition(domainObject) {
return this.getLimits(domainObject); return this.getLimits(domainObject);
}; }
/** /**
* Get a limit evaluator for this domain object. * Get a limit evaluator for this domain object.
@ -598,7 +539,7 @@ define([
* @method limitEvaluator * @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) { getLimitEvaluator(domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider) { if (!provider) {
return { return {
@ -607,7 +548,7 @@ define([
} }
return provider.getLimitEvaluator(domainObject); return provider.getLimitEvaluator(domainObject);
}; }
/** /**
* Get a limit definitions for this domain object. * Get a limit definitions for this domain object.
@ -636,7 +577,7 @@ define([
* supported colors are purple, red, orange, yellow and cyan * supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimits = function (domainObject) { getLimits(domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) { if (!provider || !provider.getLimits) {
return { return {
@ -647,7 +588,104 @@ define([
} }
return provider.getLimits(domainObject); return provider.getLimits(domainObject);
}; }
}
return TelemetryAPI; /**
}); * A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import TelemetryAPI from './TelemetryAPI'; import TelemetryAPI from './TelemetryAPI';
const { TelemetryCollection } = require("./TelemetryCollection"); import TelemetryCollection from './TelemetryCollection';
describe('Telemetry API', function () { describe('Telemetry API', function () {
let openmct; let openmct;

View File

@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro
/** Class representing a Telemetry Collection. */ /** Class representing a Telemetry Collection. */
export class TelemetryCollection extends EventEmitter { export default class TelemetryCollection extends EventEmitter {
/** /**
* Creates a Telemetry Collection * Creates a Telemetry Collection
* *
@ -127,7 +127,8 @@ export class TelemetryCollection extends EventEmitter {
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
this.emit('requestStarted'); this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options); const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options);
historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
export default class TelemetryRequestInterceptorRegistry {
/**
* A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
* requests.
* @interface TelemetryRequestInterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface TelemetryRequestInterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
* @property {function} invoke function that transforms the provided request and returns the transformed request
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct TelemetryRequestInterceptorRegistry#
*/
/**
* Register a new telemetry request interceptor.
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object/request.
* @method getInterceptors
* @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
getInterceptors(identifier, request) {
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, request);
});
}
}

View File

@ -281,11 +281,11 @@ export default {
this.xKeyOptions.push( this.xKeyOptions.push(
metadataValues.reduce((previousValue, currentValue) => { metadataValues.reduce((previousValue, currentValue) => {
return { return {
name: `${previousValue.name}, ${currentValue.name}`, name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`,
value: currentValue.key, value: currentValue.key,
isArrayValue: currentValue.isArrayValue isArrayValue: currentValue.isArrayValue
}; };
}) }, {name: ''})
); );
} }
@ -336,6 +336,8 @@ export default {
return option; return option;
}); });
} else if (this.xKey !== undefined && this.domainObject.configuration.axes.yKey === undefined) {
this.domainObject.configuration.axes.yKey = 'none';
} }
this.xKeyOptions = this.xKeyOptions.map((option, index) => { this.xKeyOptions = this.xKeyOptions.map((option, index) => {

View File

@ -367,19 +367,26 @@ describe("the plugin", function () {
type: "test-object", type: "test-object",
name: "Test Object", name: "Test Object",
telemetry: { telemetry: {
values: [{ values: [
key: "some-key", {
name: "Some attribute", key: "some-key",
hints: { source: "some-key",
domain: 1 name: "Some attribute",
} format: "enum",
}, { enumerations: [
key: "some-other-key", {
name: "Another attribute", value: 0,
hints: { string: "OFF"
range: 1 },
} {
}] value: 1,
string: "ON"
}
],
hints: {
range: 1
}
}]
} }
}; };
const composition = openmct.composition.get(parent); const composition = openmct.composition.get(parent);

View File

@ -373,39 +373,30 @@ describe("The Imagery View Layouts", () => {
return Vue.nextTick(); return Vue.nextTick();
}); });
it("on mount should show the the most recent image", () => { it("on mount should show the the most recent image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
return Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
}); });
it("on mount should show the any image layers", (done) => { it("on mount should show the any image layers", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick().then(() => { await Vue.nextTick();
Vue.nextTick(() => { const layerEls = parent.querySelectorAll('.js-layer-image');
const layerEls = parent.querySelectorAll('.js-layer-image'); console.log(layerEls);
console.log(layerEls); expect(layerEls.length).toEqual(1);
expect(layerEls.length).toEqual(1);
done();
});
});
}); });
it("should show the clicked thumbnail as the main image", (done) => { it("should show the clicked thumbnail as the main image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => { await Vue.nextTick();
const target = imageTelemetry[5].url; const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
}); });
xit("should show that an image is new", (done) => { xit("should show that an image is new", (done) => {
@ -424,71 +415,60 @@ describe("The Imagery View Layouts", () => {
}); });
}); });
it("should show that an image is not new", (done) => { it("should show that an image is not new", async () => {
Vue.nextTick(() => { await Vue.nextTick();
const target = imageTelemetry[4].url; const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { await Vue.nextTick();
const imageIsNew = isNew(parent); const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse(); expect(imageIsNew).toBeFalse();
done();
});
});
}); });
it("should navigate via arrow keys", (done) => { it("should navigate via arrow keys", async () => {
Vue.nextTick(() => { await Vue.nextTick();
let keyOpts = { const keyOpts = {
element: parent.querySelector('.c-imagery'), element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft', key: 'ArrowLeft',
keyCode: 37, keyCode: 37,
type: 'keyup' type: 'keyup'
}; };
simulateKeyEvent(keyOpts); simulateKeyEvent(keyOpts);
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
});
}); });
it("should navigate via numerous arrow keys", (done) => { it("should navigate via numerous arrow keys", async () => {
Vue.nextTick(() => { await Vue.nextTick();
let element = parent.querySelector('.c-imagery'); const element = parent.querySelector('.c-imagery');
let type = 'keyup'; const type = 'keyup';
let leftKeyOpts = { const leftKeyOpts = {
element, element,
type, type,
key: 'ArrowLeft', key: 'ArrowLeft',
keyCode: 37 keyCode: 37
}; };
let rightKeyOpts = { const rightKeyOpts = {
element, element,
type, type,
key: 'ArrowRight', key: 'ArrowRight',
keyCode: 39 keyCode: 39
}; };
// left thrice // left thrice
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
// right once // right once
simulateKeyEvent(rightKeyOpts); simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
}); });
it ('shows an auto scroll button when scroll to left', (done) => { it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => { Vue.nextTick(() => {

View File

@ -87,6 +87,7 @@
:highlights="highlights" :highlights="highlights"
:show-limit-line-labels="showLimitLineLabels" :show-limit-line-labels="showLimitLineLabels"
@plotReinitializeCanvas="initCanvas" @plotReinitializeCanvas="initCanvas"
@chartLoaded="initialize"
/> />
</div> </div>
@ -359,11 +360,6 @@ export default {
this.setTimeContext(); this.setTimeContext();
this.loaded = true; this.loaded = true;
//We're referencing the canvas elements from the mct-chart in the initialize method.
// So we need $nextTick to ensure the component is fully mounted before we can initialize stuff.
this.$nextTick(this.initialize);
}, },
beforeDestroy() { beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keydown', this.handleKeyDown);

View File

@ -115,6 +115,7 @@ export default {
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw); this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw); this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
this.config.series.forEach(this.onSeriesAdd, this); this.config.series.forEach(this.onSeriesAdd, this);
this.$emit('chartLoaded');
}, },
beforeDestroy() { beforeDestroy() {
this.destroy(); this.destroy();

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import DefaultClock from '../../utils/clock/DefaultClock'; import DefaultClock from '../../utils/clock/DefaultClock';
import remoteClockRequestInterceptor from './requestInterceptor';
/** /**
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
@ -49,6 +50,14 @@ export default class RemoteClock extends DefaultClock {
this.lastTick = 0; this.lastTick = 0;
this.openmct.telemetry.addRequestInterceptor(
remoteClockRequestInterceptor(
this.openmct,
this.identifier,
this.#waitForReady.bind(this)
)
);
this._processDatum = this._processDatum.bind(this); this._processDatum = this._processDatum.bind(this);
} }
@ -129,4 +138,25 @@ export default class RemoteClock extends DefaultClock {
return timeFormatter.parse(datum); return timeFormatter.parse(datum);
}; };
} }
/**
* Waits for the clock to have a non-default tick value.
*
* @private
*/
#waitForReady() {
const waitForInitialTick = (resolve) => {
if (this.lastTick > 0) {
const offsets = this.openmct.time.clockOffsets();
resolve({
start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end
});
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
}
};
return new Promise(waitForInitialTick);
}
} }

View File

@ -0,0 +1,46 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
function remoteClockRequestInterceptor(openmct, remoteClockIdentifier, waitForBounds) {
let remoteClockLoaded = false;
return {
appliesTo: () => {
// Get the activeClock from the Global Time Context
const { activeClock } = openmct.time.getContextForView();
return activeClock !== undefined
&& activeClock.key === 'remote-clock'
&& !remoteClockLoaded;
},
invoke: async (request) => {
const { start, end } = await waitForBounds();
remoteClockLoaded = true;
request[1].start = start;
request[1].end = end;
return request;
}
};
}
export default remoteClockRequestInterceptor;

View File

@ -78,7 +78,7 @@ class StaticModelProvider {
} }
parseTreeLeaf(leafKey, leafValue, idMap, namespace) { parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
if (!leafValue) { if (leafValue === null || leafValue === undefined) {
return leafValue; return leafValue;
} }

View File

@ -135,7 +135,7 @@ describe("the plugin", () => {
let tableInstance; let tableInstance;
let mockClock; let mockClock;
beforeEach(() => { beforeEach(async () => {
openmct.time.timeSystem('utc', { openmct.time.timeSystem('utc', {
start: 0, start: 0,
end: 4 end: 4
@ -210,16 +210,8 @@ describe("the plugin", () => {
'some-other-key': 'some-other-value 3' 'some-other-key': 'some-other-value 3'
} }
]; ];
let telemetryPromiseResolve;
let telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
historicalProvider.request = () => { historicalProvider.request = () => Promise.resolve(testTelemetry);
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
};
openmct.router.path = [testTelemetryObject]; openmct.router.path = [testTelemetryObject];
@ -230,7 +222,7 @@ describe("the plugin", () => {
tableInstance = tableView.getTable(); tableInstance = tableView.getTable();
return telemetryPromise.then(() => Vue.nextTick()); await Vue.nextTick();
}); });
afterEach(() => { afterEach(() => {
@ -255,13 +247,10 @@ describe("the plugin", () => {
}); });
it("Renders a row for every telemetry datum returned", (done) => { it("Renders a row for every telemetry datum returned", async () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
Vue.nextTick(() => { await Vue.nextTick();
expect(rows.length).toBe(3); expect(rows.length).toBe(3);
done();
});
}); });
it("Renders a column for every item in telemetry metadata", () => { it("Renders a column for every item in telemetry metadata", () => {
@ -273,7 +262,7 @@ describe("the plugin", () => {
expect(headers[3].innerText).toBe('Another attribute'); expect(headers[3].innerText).toBe('Another attribute');
}); });
it("Supports column reordering via drag and drop", () => { it("Supports column reordering via drag and drop", async () => {
let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let fromColumn = columns[0]; let fromColumn = columns[0];
let toColumn = columns[1]; let toColumn = columns[1];
@ -292,54 +281,43 @@ describe("the plugin", () => {
toColumn.dispatchEvent(dragOverEvent); toColumn.dispatchEvent(dragOverEvent);
toColumn.dispatchEvent(dropEvent); toColumn.dispatchEvent(dropEvent);
return Vue.nextTick().then(() => { await Vue.nextTick();
columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let firstColumn = columns[0]; let firstColumn = columns[0];
let secondColumn = columns[1]; let secondColumn = columns[1];
let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText; let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText; let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
expect(fromColumnText).not.toEqual(firstColumnText);
expect(fromColumnText).not.toEqual(firstColumnText); expect(fromColumnText).toEqual(secondColumnText);
expect(fromColumnText).toEqual(secondColumnText); expect(toColumnText).not.toEqual(secondColumnText);
expect(toColumnText).not.toEqual(secondColumnText); expect(toColumnText).toEqual(firstColumnText);
expect(toColumnText).toEqual(firstColumnText);
});
}); });
it("Supports filtering telemetry by regular text search", () => { it("Supports filtering telemetry by regular text search", async () => {
tableInstance.tableRows.setColumnFilter("some-key", "1"); tableInstance.tableRows.setColumnFilter("some-key", "1");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
return Vue.nextTick().then(() => { expect(filteredRowElements.length).toEqual(1);
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); tableInstance.tableRows.setColumnFilter("some-key", "");
await Vue.nextTick();
expect(filteredRowElements.length).toEqual(1); let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
tableInstance.tableRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
}); });
it("Supports filtering using Regex", () => { it("Supports filtering using Regex", async () => {
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$"); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
return Vue.nextTick().then(() => { expect(filteredRowElements.length).toEqual(0);
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
await Vue.nextTick();
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value"); expect(allRowElements.length).toEqual(3);
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
}); });
it("displays the correct number of column headers when the configuration is mutated", async () => { it("displays the correct number of column headers when the configuration is mutated", async () => {
@ -402,7 +380,7 @@ describe("the plugin", () => {
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds(); const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = { const newBounds = {
start: currentBounds.start, start: currentBounds.start,
end: currentBounds.end - 3 end: currentBounds.end - 3
@ -410,17 +388,10 @@ describe("the plugin", () => {
// Manually change the time bounds // Manually change the time bounds
openmct.time.bounds(newBounds); openmct.time.bounds(newBounds);
await Vue.nextTick(); await Vue.nextTick();
// Verify table is no longer paused // Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull(); expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
}); });
it("Unpauses the table on user bounds change if paused by button", async () => { it("Unpauses the table on user bounds change if paused by button", async () => {
@ -428,19 +399,18 @@ describe("the plugin", () => {
// Pause by button // Pause by button
viewContext.togglePauseByButton(); viewContext.togglePauseByButton();
await Vue.nextTick(); await Vue.nextTick();
// Verify table is paused // Verify table is paused
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds(); const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = { const newBounds = {
start: currentBounds.start, start: currentBounds.start,
end: currentBounds.end - 3 end: currentBounds.end - 1
}; };
// Manually change the time bounds // Manually change the time bounds
openmct.time.bounds(newBounds); openmct.time.bounds(newBounds);
@ -448,12 +418,6 @@ describe("the plugin", () => {
// Verify table is no longer paused // Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull(); expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
}); });
it("Does not unpause the table on tick", async () => { it("Does not unpause the table on tick", async () => {

View File

@ -7,12 +7,19 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader'); const {VueLoaderPlugin} = require('vue-loader');
const gitRevision = require('child_process') let gitRevision = 'error-retrieving-revision';
.execSync('git rev-parse HEAD') let gitBranch = 'error-retrieving-branch';
.toString().trim();
const gitBranch = require('child_process') try {
.execSync('git rev-parse --abbrev-ref HEAD') gitRevision = require('child_process')
.toString().trim(); .execSync('git rev-parse HEAD')
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
} catch (err) {
console.warn(err);
}
/** @type {import('webpack').Configuration} */ /** @type {import('webpack').Configuration} */
const config = { const config = {