diff --git a/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js new file mode 100644 index 0000000000..f9167d9e4e --- /dev/null +++ b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js @@ -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 + }); +}); diff --git a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js index cd8c1c7a19..192c116283 100644 --- a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -24,7 +24,7 @@ const { test } = require('../../../fixtures'); const { expect } = require('@playwright/test'); 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({ type: 'issue', description: 'https://github.com/nasa/openmct/issues/5113' @@ -71,25 +71,34 @@ test.describe('Telemetry Table', () => { ]); // 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(); - const tableWrapper = await page.locator('div.c-table-wrapper'); + const tableWrapper = page.locator('div.c-table-wrapper'); 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); await endTimeInput.click(); let endDate = await endTimeInput.inputValue(); 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(endDate); await page.keyboard.press('Enter'); 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); }); }); diff --git a/src/MCT.js b/src/MCT.js index 8d7f252f31..b7f0e40d5a 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -203,7 +203,7 @@ define([ * @memberof module:openmct.MCT# * @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. diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index c846ddd1a4..824172d691 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -20,122 +20,18 @@ * 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([ - '../../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~ - */ +export default class 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 - */ - function TelemetryAPI(openmct) { + constructor(openmct) { this.openmct = openmct; this.formatMapCache = new WeakMap(); @@ -148,12 +44,14 @@ define([ this.requestProviders = []; this.subscriptionProviders = []; this.valueFormatterCache = new WeakMap(); + + this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry(); } - TelemetryAPI.prototype.abortAllRequests = function () { + abortAllRequests() { this.requestAbortControllers.forEach((controller) => controller.abort()); this.requestAbortControllers.clear(); - }; + } /** * Return Custom String Formatter @@ -162,9 +60,9 @@ define([ * @param {string} format custom formatter string (eg: %.4f, <s etc.) * @returns {CustomStringFormatter} */ - TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { + customStringFormatter(valueMetadata, format) { return new CustomStringFormatter.default(this.openmct, valueMetadata, format); - }; + } /** * Return true if the given domainObject is a telemetry object. A telemetry @@ -174,9 +72,9 @@ define([ * @param {module:openmct.DomainObject} domainObject * @returns {boolean} true if the object is a telemetry object. */ - TelemetryAPI.prototype.isTelemetryObject = function (domainObject) { + isTelemetryObject(domainObject) { return Boolean(this.findMetadataProvider(domainObject)); - }; + } /** * Check if this provider can supply telemetry data associated with @@ -188,10 +86,10 @@ define([ * @returns {boolean} true if telemetry can be provided * @memberof module:openmct.TelemetryAPI~TelemetryProvider# */ - TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { + canProvideTelemetry(domainObject) { return Boolean(this.findSubscriptionProvider(domainObject)) - || Boolean(this.findRequestProvider(domainObject)); - }; + || Boolean(this.findRequestProvider(domainObject)); + } /** * Register a telemetry provider with the telemetry service. This @@ -201,7 +99,7 @@ define([ * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new * telemetry provider */ - TelemetryAPI.prototype.addProvider = function (provider) { + addProvider(provider) { if (provider.supportsRequest) { this.requestProviders.unshift(provider); } @@ -217,54 +115,54 @@ define([ if (provider.supportsLimits) { this.limitProviders.unshift(provider); } - }; + } /** * @private */ - TelemetryAPI.prototype.findSubscriptionProvider = function () { + findSubscriptionProvider() { const args = Array.prototype.slice.apply(arguments); function supportsDomainObject(provider) { return provider.supportsSubscribe.apply(provider, args); } return this.subscriptionProviders.filter(supportsDomainObject)[0]; - }; + } /** * @private */ - TelemetryAPI.prototype.findRequestProvider = function (domainObject) { + findRequestProvider(domainObject) { const args = Array.prototype.slice.apply(arguments); function supportsDomainObject(provider) { return provider.supportsRequest.apply(provider, args); } return this.requestProviders.filter(supportsDomainObject)[0]; - }; + } /** * @private */ - TelemetryAPI.prototype.findMetadataProvider = function (domainObject) { + findMetadataProvider(domainObject) { return this.metadataProviders.filter(function (p) { return p.supportsMetadata(domainObject); })[0]; - }; + } /** * @private */ - TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) { + findLimitEvaluator(domainObject) { return this.limitProviders.filter(function (p) { return p.supportsLimits(domainObject); })[0]; - }; + } /** * @private */ - TelemetryAPI.prototype.standardizeRequestOptions = function (options) { + standardizeRequestOptions(options) { if (!Object.prototype.hasOwnProperty.call(options, 'start')) { options.start = this.openmct.time.bounds().start; } @@ -276,7 +174,47 @@ define([ if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { 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. @@ -292,13 +230,13 @@ define([ * options for this telemetry collection request * @returns {TelemetryCollection} a TelemetryCollection instance */ - TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { + requestCollection(domainObject, options = {}) { return new TelemetryCollection( this.openmct, domainObject, options ); - }; + } /** * Request historical telemetry for a domain object. @@ -315,7 +253,7 @@ define([ * @returns {Promise.} a promise for an array of * telemetry data */ - TelemetryAPI.prototype.request = function (domainObject) { + async request(domainObject) { if (this.noRequestProviderForAllObjects) { return Promise.resolve([]); } @@ -330,6 +268,7 @@ define([ this.requestAbortControllers.add(abortController); this.standardizeRequestOptions(arguments[1]); + const provider = this.findRequestProvider.apply(this, arguments); if (!provider) { this.requestAbortControllers.delete(abortController); @@ -337,6 +276,8 @@ define([ return this.handleMissingRequestProvider(domainObject); } + arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]); + return provider.request.apply(provider, arguments) .catch((rejected) => { if (rejected.name !== 'AbortError') { @@ -348,7 +289,7 @@ define([ }).finally(() => { this.requestAbortControllers.delete(abortController); }); - }; + } /** * Subscribe to realtime telemetry for a specific domain object. @@ -364,7 +305,7 @@ define([ * @returns {Function} a function which may be called to terminate * the subscription */ - TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) { + subscribe(domainObject, callback, options) { const provider = this.findSubscriptionProvider(domainObject); if (!this.subscribeCache) { @@ -401,7 +342,7 @@ define([ delete this.subscribeCache[keyString]; } }.bind(this); - }; + } /** * Get telemetry metadata for a given domain object. Returns a telemetry @@ -410,7 +351,7 @@ define([ * * @returns {TelemetryMetadataManager} */ - TelemetryAPI.prototype.getMetadata = function (domainObject) { + getMetadata(domainObject) { if (!this.metadataCache.has(domainObject)) { const metadataProvider = this.findMetadataProvider(domainObject); if (!metadataProvider) { @@ -426,14 +367,14 @@ define([ } return this.metadataCache.get(domainObject); - }; + } /** * Return an array of valueMetadatas that are common to all supplied * telemetry objects and match the requested hints. * */ - TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { + commonValuesForHints(metadatas, hints) { const options = metadatas.map(function (metadata) { const values = metadata.valuesForHints(hints); @@ -453,14 +394,14 @@ define([ }); return _.sortBy(options, sortKeys); - }; + } /** * Get a value formatter for a given valueMetadata. * * @returns {TelemetryValueFormatter} */ - TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { + getValueFormatter(valueMetadata) { if (!this.valueFormatterCache.has(valueMetadata)) { this.valueFormatterCache.set( valueMetadata, @@ -469,7 +410,7 @@ define([ } return this.valueFormatterCache.get(valueMetadata); - }; + } /** * Get a value formatter for a given key. @@ -477,9 +418,9 @@ define([ * * @returns {Format} */ - TelemetryAPI.prototype.getFormatter = function (key) { + getFormatter(key) { return this.formatters.get(key); - }; + } /** * Get a format map of all value formatters for a given piece of telemetry @@ -487,7 +428,7 @@ define([ * * @returns {Object} */ - TelemetryAPI.prototype.getFormatMap = function (metadata) { + getFormatMap(metadata) { if (!metadata) { return {}; } @@ -502,14 +443,14 @@ define([ } return this.formatMapCache.get(metadata); - }; + } /** * Error Handling: Missing Request provider * * @returns Promise */ - TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { + handleMissingRequestProvider(domainObject) { this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; @@ -532,15 +473,15 @@ define([ console.warn(detailMessage); return Promise.resolve([]); - }; + } /** * Register a new telemetry data formatter. * @param {Format} format the */ - TelemetryAPI.prototype.addFormat = function (format) { + addFormat(format) { this.formatters.set(format.key, format); - }; + } /** * Get a limit evaluator for this domain object. @@ -558,9 +499,9 @@ define([ * @method limitEvaluator * @memberof module:openmct.TelemetryAPI~TelemetryProvider# */ - TelemetryAPI.prototype.limitEvaluator = function (domainObject) { + limitEvaluator(domainObject) { return this.getLimitEvaluator(domainObject); - }; + } /** * Get a limits for this domain object. @@ -578,9 +519,9 @@ define([ * @method limits * @memberof module:openmct.TelemetryAPI~TelemetryProvider# */ - TelemetryAPI.prototype.limitDefinition = function (domainObject) { + limitDefinition(domainObject) { return this.getLimits(domainObject); - }; + } /** * Get a limit evaluator for this domain object. @@ -598,7 +539,7 @@ define([ * @method limitEvaluator * @memberof module:openmct.TelemetryAPI~TelemetryProvider# */ - TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) { + getLimitEvaluator(domainObject) { const provider = this.findLimitEvaluator(domainObject); if (!provider) { return { @@ -607,7 +548,7 @@ define([ } return provider.getLimitEvaluator(domainObject); - }; + } /** * Get a limit definitions for this domain object. @@ -636,7 +577,7 @@ define([ * supported colors are purple, red, orange, yellow and cyan * @memberof module:openmct.TelemetryAPI~TelemetryProvider# */ - TelemetryAPI.prototype.getLimits = function (domainObject) { + getLimits(domainObject) { const provider = this.findLimitEvaluator(domainObject); if (!provider || !provider.getLimits) { return { @@ -647,7 +588,104 @@ define([ } 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 + */ diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js index 0b3c91533f..5af9440098 100644 --- a/src/api/telemetry/TelemetryAPISpec.js +++ b/src/api/telemetry/TelemetryAPISpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ import { createOpenMct, resetApplicationState } from 'utils/testing'; import TelemetryAPI from './TelemetryAPI'; -const { TelemetryCollection } = require("./TelemetryCollection"); +import TelemetryCollection from './TelemetryCollection'; describe('Telemetry API', function () { let openmct; diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 97aa73493a..1ac9fc5872 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro /** Class representing a Telemetry Collection. */ -export class TelemetryCollection extends EventEmitter { +export default class TelemetryCollection extends EventEmitter { /** * Creates a Telemetry Collection * @@ -127,7 +127,8 @@ export class TelemetryCollection extends EventEmitter { this.requestAbort = new AbortController(); options.signal = this.requestAbort.signal; 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) { if (error.name !== 'AbortError') { console.error('Error requesting telemetry data...'); diff --git a/src/api/telemetry/TelemetryRequestInterceptor.js b/src/api/telemetry/TelemetryRequestInterceptor.js new file mode 100644 index 0000000000..7204ee3321 --- /dev/null +++ b/src/api/telemetry/TelemetryRequestInterceptor.js @@ -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); + }); + } + +} + diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index 0eddb3ce76..e595117a82 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -373,39 +373,30 @@ describe("The Imagery View Layouts", () => { 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 - return Vue.nextTick(() => { - const imageInfo = getImageInfo(parent); - - expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); - }); + await Vue.nextTick(); + const imageInfo = getImageInfo(parent); + 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 - Vue.nextTick().then(() => { - Vue.nextTick(() => { - const layerEls = parent.querySelectorAll('.js-layer-image'); - console.log(layerEls); - expect(layerEls.length).toEqual(1); - done(); - }); - }); + await Vue.nextTick(); + const layerEls = parent.querySelectorAll('.js-layer-image'); + console.log(layerEls); + expect(layerEls.length).toEqual(1); }); - 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 - Vue.nextTick(() => { - const target = imageTelemetry[5].url; - parent.querySelectorAll(`img[src='${target}']`)[0].click(); - Vue.nextTick(() => { - const imageInfo = getImageInfo(parent); + await Vue.nextTick(); + const target = imageTelemetry[5].url; + parent.querySelectorAll(`img[src='${target}']`)[0].click(); + await Vue.nextTick(); + const imageInfo = getImageInfo(parent); - expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); - done(); - }); - }); + expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); }); 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) => { - Vue.nextTick(() => { - const target = imageTelemetry[4].url; - parent.querySelectorAll(`img[src='${target}']`)[0].click(); + it("should show that an image is not new", async () => { + await Vue.nextTick(); + const target = imageTelemetry[4].url; + parent.querySelectorAll(`img[src='${target}']`)[0].click(); - Vue.nextTick(() => { - const imageIsNew = isNew(parent); + await Vue.nextTick(); + const imageIsNew = isNew(parent); - expect(imageIsNew).toBeFalse(); - done(); - }); - }); + expect(imageIsNew).toBeFalse(); }); - it("should navigate via arrow keys", (done) => { - Vue.nextTick(() => { - let keyOpts = { - element: parent.querySelector('.c-imagery'), - key: 'ArrowLeft', - keyCode: 37, - type: 'keyup' - }; + it("should navigate via arrow keys", async () => { + await Vue.nextTick(); + const keyOpts = { + element: parent.querySelector('.c-imagery'), + key: 'ArrowLeft', + keyCode: 37, + type: 'keyup' + }; - simulateKeyEvent(keyOpts); + simulateKeyEvent(keyOpts); - Vue.nextTick(() => { - const imageInfo = getImageInfo(parent); - - expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); - done(); - }); - }); + await Vue.nextTick(); + const imageInfo = getImageInfo(parent); + expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); }); - it("should navigate via numerous arrow keys", (done) => { - Vue.nextTick(() => { - let element = parent.querySelector('.c-imagery'); - let type = 'keyup'; - let leftKeyOpts = { - element, - type, - key: 'ArrowLeft', - keyCode: 37 - }; - let rightKeyOpts = { - element, - type, - key: 'ArrowRight', - keyCode: 39 - }; + it("should navigate via numerous arrow keys", async () => { + await Vue.nextTick(); + const element = parent.querySelector('.c-imagery'); + const type = 'keyup'; + const leftKeyOpts = { + element, + type, + key: 'ArrowLeft', + keyCode: 37 + }; + const rightKeyOpts = { + element, + type, + key: 'ArrowRight', + keyCode: 39 + }; - // left thrice - simulateKeyEvent(leftKeyOpts); - simulateKeyEvent(leftKeyOpts); - simulateKeyEvent(leftKeyOpts); - // right once - simulateKeyEvent(rightKeyOpts); + // left thrice + simulateKeyEvent(leftKeyOpts); + simulateKeyEvent(leftKeyOpts); + simulateKeyEvent(leftKeyOpts); + // right once + simulateKeyEvent(rightKeyOpts); - Vue.nextTick(() => { - const imageInfo = getImageInfo(parent); - - expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); - done(); - }); - }); + await Vue.nextTick(); + const imageInfo = getImageInfo(parent); + expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); }); it ('shows an auto scroll button when scroll to left', (done) => { Vue.nextTick(() => { diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js index 1502d9a9dd..3d6e6fcf4f 100644 --- a/src/plugins/remoteClock/RemoteClock.js +++ b/src/plugins/remoteClock/RemoteClock.js @@ -20,6 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import DefaultClock from '../../utils/clock/DefaultClock'; +import remoteClockRequestInterceptor from './requestInterceptor'; /** * 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.openmct.telemetry.addRequestInterceptor( + remoteClockRequestInterceptor( + this.openmct, + this.identifier, + this.#waitForReady.bind(this) + ) + ); + this._processDatum = this._processDatum.bind(this); } @@ -129,4 +138,25 @@ export default class RemoteClock extends DefaultClock { 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); + } } diff --git a/src/plugins/remoteClock/requestInterceptor.js b/src/plugins/remoteClock/requestInterceptor.js new file mode 100644 index 0000000000..bb38031bdd --- /dev/null +++ b/src/plugins/remoteClock/requestInterceptor.js @@ -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; diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index 9d8892f3c2..19cdd7e9d6 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -135,7 +135,7 @@ describe("the plugin", () => { let tableInstance; let mockClock; - beforeEach(() => { + beforeEach(async () => { openmct.time.timeSystem('utc', { start: 0, end: 4 @@ -210,16 +210,8 @@ describe("the plugin", () => { 'some-other-key': 'some-other-value 3' } ]; - let telemetryPromiseResolve; - let telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; - }); - historicalProvider.request = () => { - telemetryPromiseResolve(testTelemetry); - - return telemetryPromise; - }; + historicalProvider.request = () => Promise.resolve(testTelemetry); openmct.router.path = [testTelemetryObject]; @@ -230,7 +222,7 @@ describe("the plugin", () => { tableInstance = tableView.getTable(); - return telemetryPromise.then(() => Vue.nextTick()); + await Vue.nextTick(); }); 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'); - Vue.nextTick(() => { - expect(rows.length).toBe(3); - - done(); - }); + await Vue.nextTick(); + expect(rows.length).toBe(3); }); it("Renders a column for every item in telemetry metadata", () => { @@ -273,7 +262,7 @@ describe("the plugin", () => { 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 fromColumn = columns[0]; let toColumn = columns[1]; @@ -292,54 +281,43 @@ describe("the plugin", () => { toColumn.dispatchEvent(dragOverEvent); toColumn.dispatchEvent(dropEvent); - return Vue.nextTick().then(() => { - columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); - let firstColumn = columns[0]; - let secondColumn = columns[1]; - let firstColumnText = firstColumn.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).toEqual(secondColumnText); - expect(toColumnText).not.toEqual(secondColumnText); - expect(toColumnText).toEqual(firstColumnText); - }); + await Vue.nextTick(); + columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); + let firstColumn = columns[0]; + let secondColumn = columns[1]; + let firstColumnText = firstColumn.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).toEqual(secondColumnText); + expect(toColumnText).not.toEqual(secondColumnText); + 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"); + await Vue.nextTick(); + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - return Vue.nextTick().then(() => { - let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + expect(filteredRowElements.length).toEqual(1); + tableInstance.tableRows.setColumnFilter("some-key", ""); + await Vue.nextTick(); - expect(filteredRowElements.length).toEqual(1); - - tableInstance.tableRows.setColumnFilter("some-key", ""); - - return Vue.nextTick().then(() => { - let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(allRowElements.length).toEqual(3); - }); - }); + 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$"); + await Vue.nextTick(); + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - return Vue.nextTick().then(() => { - let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + expect(filteredRowElements.length).toEqual(0); - 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"); - - return Vue.nextTick().then(() => { - let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(allRowElements.length).toEqual(3); - }); - }); + expect(allRowElements.length).toEqual(3); }); 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(); const currentBounds = openmct.time.bounds(); - + await Vue.nextTick(); const newBounds = { start: currentBounds.start, end: currentBounds.end - 3 @@ -410,17 +388,10 @@ describe("the plugin", () => { // Manually change the time bounds openmct.time.bounds(newBounds); - await Vue.nextTick(); // Verify table is no longer paused 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 () => { @@ -428,19 +399,18 @@ describe("the plugin", () => { // Pause by button viewContext.togglePauseByButton(); - await Vue.nextTick(); // Verify table is paused expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); const currentBounds = openmct.time.bounds(); + await Vue.nextTick(); const newBounds = { start: currentBounds.start, - end: currentBounds.end - 3 + end: currentBounds.end - 1 }; - // Manually change the time bounds openmct.time.bounds(newBounds); @@ -448,12 +418,6 @@ describe("the plugin", () => { // Verify table is no longer paused 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 () => { diff --git a/webpack.common.js b/webpack.common.js index 1c938af845..95a76fd404 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -7,12 +7,19 @@ const webpack = require('webpack'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const {VueLoaderPlugin} = require('vue-loader'); -const gitRevision = require('child_process') - .execSync('git rev-parse HEAD') - .toString().trim(); -const gitBranch = require('child_process') - .execSync('git rev-parse --abbrev-ref HEAD') - .toString().trim(); +let gitRevision = 'error-retrieving-revision'; +let gitBranch = 'error-retrieving-branch'; + +try { + gitRevision = require('child_process') + .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} */ const config = {