From a58aba5c4972ebd9eb6fca61ec7450c5bfbd35b0 Mon Sep 17 00:00:00 2001 From: fubaa Date: Thu, 25 Jan 2024 06:49:49 +0530 Subject: [PATCH] Major features | Update package.json version to relate it with pixxelhq/yamcs 1. Add mechanism to authenticate the plugin against a username/password protected yamcs server. 2. Modify parameter loading from the MDb, accounting for commandHistory parameters. --- src/actions/exportToCSV/ExportToCSVAction.js | 4 +- src/const.js | 3 +- .../fault-action-provider.js | 3 +- .../historical-fault-provider.js | 3 +- src/providers/latest-telemetry-provider.js | 5 +- src/providers/object-provider.js | 23 +++-- src/providers/realtime-provider.js | 7 +- .../user/operator-status-telemetry.js | 3 +- src/providers/user/poll-question-telemetry.js | 3 +- src/providers/user/user-provider.js | 3 +- src/utils.js | 98 ++++++++++++++++++- 11 files changed, 134 insertions(+), 21 deletions(-) diff --git a/src/actions/exportToCSV/ExportToCSVAction.js b/src/actions/exportToCSV/ExportToCSVAction.js index 4089de39..8dbb10fa 100644 --- a/src/actions/exportToCSV/ExportToCSVAction.js +++ b/src/actions/exportToCSV/ExportToCSVAction.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import { OBJECT_TYPES } from "../../const.js"; -import {idToQualifiedName} from "../../utils.js"; +import { customFetch, idToQualifiedName } from "../../utils.js"; import { saveAs } from 'file-saver'; const SUPPORTED_TYPES = [OBJECT_TYPES.TELEMETRY_OBJECT_TYPE, OBJECT_TYPES.AGGREGATE_TELEMETRY_TYPE]; @@ -115,7 +115,7 @@ export default class ExportToCSVAction { url += `¶meters=${parameterIdsString}`; url += `&delimiter=COMMA`; - const response = await fetch(url); + const response = await customFetch(url); if (!response.ok) { return response.json(); } else { diff --git a/src/const.js b/src/const.js index 9eda3ac0..3c837520 100644 --- a/src/const.js +++ b/src/const.js @@ -30,7 +30,8 @@ export const OBJECT_TYPES = { OPERATOR_STATUS_TYPE: 'yamcs.operatorStatus', POLL_QUESTION_TYPE: 'yamcs.pollQuestion', ALARMS_TYPE: 'yamcs.alarms', - GLOBAL_STATUS_TYPE: 'yamcs.globalStatus' + GLOBAL_STATUS_TYPE: 'yamcs.globalStatus', + COMMAND_HISTORY_TYPE: 'yamcs.cmdHist' }; export const MDB_TYPE = 'yamcs.mdbchanges'; diff --git a/src/providers/fault-mgmt-providers/fault-action-provider.js b/src/providers/fault-mgmt-providers/fault-action-provider.js index 61e83b9e..3afb58bf 100644 --- a/src/providers/fault-mgmt-providers/fault-action-provider.js +++ b/src/providers/fault-mgmt-providers/fault-action-provider.js @@ -1,3 +1,4 @@ +import { customFetch } from '../../utils.js'; import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_DEFAULT_SHELVE_DURATION } from './fault-mgmt-constants.js'; export default class FaultActionProvider { @@ -55,6 +56,6 @@ export default class FaultActionProvider { } _sendRequest(url, options) { - return fetch(url, options); + return customFetch(url, options); } } diff --git a/src/providers/fault-mgmt-providers/historical-fault-provider.js b/src/providers/fault-mgmt-providers/historical-fault-provider.js index c7a5ce10..0e8c8952 100644 --- a/src/providers/fault-mgmt-providers/historical-fault-provider.js +++ b/src/providers/fault-mgmt-providers/historical-fault-provider.js @@ -1,3 +1,4 @@ +import { customFetch } from '../../utils.js'; import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_TYPE } from './fault-mgmt-constants.js'; export default class HistoricalFaultProvider { @@ -15,7 +16,7 @@ export default class HistoricalFaultProvider { async request() { let url = `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MANAGEMENT_ALARMS}`; - const res = await fetch(url); + const res = await customFetch(url); const faultsData = await res.json(); return faultsData.alarms?.map(this.faultModelConverter); diff --git a/src/providers/latest-telemetry-provider.js b/src/providers/latest-telemetry-provider.js index 36fe7c7d..d25ef573 100644 --- a/src/providers/latest-telemetry-provider.js +++ b/src/providers/latest-telemetry-provider.js @@ -23,7 +23,8 @@ import { getValue, idToQualifiedName, qualifiedNameFromParameterId, - qualifiedNameToId + qualifiedNameToId, + customFetch } from '../utils.js'; const BATCH_DEBOUNCE_MS = 100; @@ -98,7 +99,7 @@ export default class LatestTelemetryProvider { fromCache: true }; - const response = await fetch(this.#buildUrl(), { + const response = await customFetch(this.#buildUrl(), { method: 'POST', body: JSON.stringify(requestBody) }); diff --git a/src/providers/object-provider.js b/src/providers/object-provider.js index de499d49..f7188d09 100644 --- a/src/providers/object-provider.js +++ b/src/providers/object-provider.js @@ -190,7 +190,7 @@ export default class YamcsObjectProvider { } async #loadTelemetryDictionary() { - const operation = 'parameters?details=yes&limit=1000'; + const operation = 'parameters?details=yes'; const parameterUrl = this.url + 'api/mdb/' + this.instance + '/' + operation; const url = this.#getMdbUrl('space-systems'); const spaceSystems = await accumulateResults(url, {}, 'spaceSystems', []); @@ -370,15 +370,18 @@ export default class YamcsObjectProvider { } const isAggregate = this.#isAggregate(parameter); + const isCommandHistory = this.#isCommandHistory(parameter); let aggregateHasMembers = false; - if (this.limitOverrides[qualifiedName] !== undefined) { - obj.configuration.limits = this.limitOverrides[qualifiedName]; - } else if (parameter.type.defaultAlarm) { - obj.configuration.limits = this.#convertToLimits(parameter.type.defaultAlarm); + if(!isCommandHistory) { + if (this.limitOverrides[qualifiedName] !== undefined) { + obj.configuration.limits = this.limitOverrides[qualifiedName]; + } else if (parameter.type.defaultAlarm) { + obj.configuration.limits = this.#convertToLimits(parameter.type.defaultAlarm); + } } - if (!isAggregate) { + if (!isAggregate && !isCommandHistory) { const key = 'value'; const telemetryValue = { key, @@ -476,6 +479,10 @@ export default class YamcsObjectProvider { return parameter?.type?.engType === 'aggregate'; } + #isCommandHistory(parameter) { + return parameter?.dataSource === 'COMMAND' || parameter?.dataSource === 'COMMAND_HISTORY'; + } + #isImage(obj) { return (obj.type === OBJECT_TYPES.IMAGE_OBJECT_TYPE); } @@ -542,6 +549,10 @@ export default class YamcsObjectProvider { } } + if(this.#isCommandHistory(parameter)) { + return OBJECT_TYPES.COMMAND_HISTORY_TYPE; + } + if (this.#isAggregate(parameter) && this.#aggregateHasMembers(parameter)) { return OBJECT_TYPES.AGGREGATE_TELEMETRY_TYPE; } diff --git a/src/providers/realtime-provider.js b/src/providers/realtime-provider.js index 2a00c09b..e1b8887c 100644 --- a/src/providers/realtime-provider.js +++ b/src/providers/realtime-provider.js @@ -208,9 +208,14 @@ export default class RealtimeProvider { return; } - let wsUrl = `${this.url}`; this.lastSubscriptionId = 1; this.connected = false; + + this.postConnect(); + } + + postConnect(access_token) { + let wsUrl = `${this.url}`; this.socket = new WebSocket(wsUrl); this.socket.onopen = () => { diff --git a/src/providers/user/operator-status-telemetry.js b/src/providers/user/operator-status-telemetry.js index de4b9bd5..50379e05 100644 --- a/src/providers/user/operator-status-telemetry.js +++ b/src/providers/user/operator-status-telemetry.js @@ -20,6 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import { + customFetch, idToQualifiedName } from '../../utils.js'; @@ -50,7 +51,7 @@ export default class OperatorStatusTelemetry { let success = false; try { - const result = await fetch(setParameterUrl, { + const result = await customFetch(setParameterUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' diff --git a/src/providers/user/poll-question-telemetry.js b/src/providers/user/poll-question-telemetry.js index b8597793..6f970df2 100644 --- a/src/providers/user/poll-question-telemetry.js +++ b/src/providers/user/poll-question-telemetry.js @@ -20,6 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import { + customFetch, idToQualifiedName } from '../../utils.js'; @@ -54,7 +55,7 @@ export default class PollQuestionTelemetry { let success = false; try { - const result = await fetch(setParameterUrl, { + const result = await customFetch(setParameterUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' diff --git a/src/providers/user/user-provider.js b/src/providers/user/user-provider.js index ab93ec9d..00248fa9 100644 --- a/src/providers/user/user-provider.js +++ b/src/providers/user/user-provider.js @@ -22,6 +22,7 @@ import createYamcsUser from './createYamcsUser.js'; import { EventEmitter } from 'eventemitter3'; +import { customFetch } from '../../utils.js'; export default class UserProvider extends EventEmitter { constructor(openmct, {userEndpoint, roleStatus, latestTelemetryProvider, realtimeTelemetryProvider, pollQuestionParameter, pollQuestionTelemetry}) { @@ -187,7 +188,7 @@ export default class UserProvider extends EventEmitter { async #getUserInfo() { try { - const res = await fetch(this.userEndpoint); + const res = await customFetch(this.userEndpoint); const info = await res.json(); this.user = new this.YamcsUser(info); diff --git a/src/utils.js b/src/utils.js index 7f268d37..37f7f0a5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -184,7 +184,8 @@ async function accumulateResults(url, options, property, soFar, totalLimit, toke let newUrl = formatUrl(url, token); - const fetchResult = await fetch(newUrl, options); + const fetchResult = await customFetch(newUrl, options); + const result = await fetchResult.json(); if (property in result) { @@ -200,7 +201,8 @@ async function accumulateResults(url, options, property, soFar, totalLimit, toke } async function requestLimitOverrides(url) { - const response = await fetch(encodeURI(url)); + const response = await customFetch(encodeURI(url)); + const json = await response.json(); return json?.overrides ?? []; @@ -295,7 +297,7 @@ function getHistoryYieldRequest(signal) { while (url) { url = yield; - yield fetch(url, { signal}) + yield customFetch(url, { signal}) .then(res => res.json()); } } @@ -362,6 +364,93 @@ function flattenObjectArray(array, baseObj = {}) { }, baseObj); } +async function getAccessToken(forceRefresh=false) { + const tokenUrl = "http://" + process.env.YAMCS_ENDPOINT + "/auth/token"; + const params = new URLSearchParams(); + + const expiration_time = getCookie("expiration_time"); + + if (!forceRefresh && expiration_time != null && Date.now() > expiration_time) { + params.append("refresh_token", getCookie("refresh_token")); + params.append("grant_type", "refresh_token"); + + } else { + params.append("username", process.env.YAMCS_USERNAME); + params.append("password", process.env.YAMCS_PASSWORD); + params.append("grant_type", "password"); + } + + fetch(tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to retrieve API token: ${response.status}`); + } + + return response.json(); + }) + .then(data => { + const access_token = data.access_token; + const refresh_token = data.refresh_token; + + const lastLoginTime = new Date(data.user.lastLoginTime).getTime(); + const expires_in = parseInt(data.expires_in) * 1000; + + document.cookie = `access_token=${access_token}; path=/; SameSite=Strict;`; + document.cookie = `refresh_token=${refresh_token}; path=/; SameSite=Strict;`; + document.cookie = `expiration_time=${lastLoginTime + expires_in}; path=/; SameSite=Strict;`; + }); +} + +function getCookie(name) { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [cookieName, cookieValue] = cookie.trim().split('='); + + if (cookieName === name) { + return decodeURIComponent(cookieValue); + } + } + return null; // Return null if the cookie is not found +} + +async function customFetch(url, requestOptions = {}) { + const access_token = getCookie('access_token'); + + // Extend the provided requestOptions object with headers + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${access_token}` + }; + + // Merge the headers with the provided requestOptions headers + requestOptions.headers = { + ...requestOptions.headers, + ...headers + }; + + const response = fetch(url, requestOptions) + .then(response => { + if (!response.ok) { + if (response.status === 401){ + getAccessToken(true); + return customFetch(url, requestOptions); + } + else { + throw new Error(`Failed to execute API: ${response}`); + } + } + return response; + }); + + return response; +} + export { buildStalenessResponseObject, getLimitFromAlarmRange, @@ -373,5 +462,6 @@ export { accumulateResults, addLimitInformation, yieldResults, - getLimitOverrides + getLimitOverrides, + customFetch, };