diff --git a/example/index.js b/example/index.js index 3b968793..86e4954e 100644 --- a/example/index.js +++ b/example/index.js @@ -115,6 +115,9 @@ const openmct = window.openmct; openmct.install(openmct.plugins.FaultManagement()); openmct.install(openmct.plugins.BarChart()); + const timeLinePlugin = openmct.plugins.Timeline(); + openmct.install(timeLinePlugin); + openmct.install(openmct.plugins.EventTimestripPlugin(timeLinePlugin.extendedLinesBus)); // setup example display layout openmct.on('start', async () => { diff --git a/example/make-example-events.mjs b/example/make-example-events.mjs new file mode 100644 index 00000000..aa8ab162 --- /dev/null +++ b/example/make-example-events.mjs @@ -0,0 +1,127 @@ +import process from 'process'; + +const INSTANCE = "myproject"; +const URL = `http://localhost:8090/api/archive/${INSTANCE}/events`; + +const events = [ + { + type: "PRESSURE_ALERT", + message: "Pressure threshold exceeded", + severity: "CRITICAL", + source: "PressureModule", + sequenceNumber: 1, + extra: { + pressure: "150 PSI", + location: "Hydraulic System" + } + }, + { + type: "PRESSURE_WARNING", + message: "Pressure nearing critical level", + severity: "WARNING", + source: "PressureModule", + sequenceNumber: 2, + extra: { + pressure: "140 PSI", + location: "Hydraulic System" + } + }, + { + type: "PRESSURE_INFO", + message: "Pressure system check completed", + severity: "INFO", + source: "PressureModule", + sequenceNumber: 3, + extra: { + checkType: "Routine Inspection", + duration: "10m" + } + }, + { + type: "TEMPERATURE_ALERT", + message: "Temperature threshold exceeded", + severity: "CRITICAL", + source: "TemperatureModule", + sequenceNumber: 4, + extra: { + temperature: "100°C", + location: "Engine Room" + } + }, + { + type: "TEMPERATURE_WARNING", + message: "Temperature nearing critical level", + severity: "WARNING", + source: "TemperatureModule", + sequenceNumber: 5, + extra: { + temperature: "95°C", + location: "Engine Room" + } + }, + { + type: "TEMPERATURE_INFO", + message: "Temperature nominal", + severity: "INFO", + source: "TemperatureModule", + sequenceNumber: 6, + extra: { + temperature: "35°C", + location: "Life Support" + } + }, + { + type: "TEMPERATURE_INFO", + message: "Temperature nominal", + severity: "INFO", + source: "TemperatureModule", + sequenceNumber: 7, + extra: { + temperature: "30°C", + location: "Life Support" + } + }, + { + type: "TEMPERATURE_SEVERE", + message: "Temperature nominal", + severity: "SEVERE", + source: "TemperatureModule", + sequenceNumber: 8, + extra: { + temperature: "200°C", + location: "Engine Room" + } + } +]; + +async function postEvent(event, delaySeconds) { + const eventTime = new Date(Date.now() + delaySeconds * 1000).toISOString(); + event.time = eventTime; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(event) + }); + + if (response.ok) { + console.log(`Event posted successfully: ${event.type}`); + } else { + console.error(`Failed to post event: ${event.type}. HTTP Status: ${response.status}`); + } + } catch (error) { + console.error(`Error posting event: ${event.type}.`, error); + } +} + +export async function postAllEvents() { + for (let i = 0; i < events.length; i++) { + await postEvent(events[i], i * 5); + } +} + +// If you still want to run it standalone +if (import.meta.url === `file://${process.argv[1]}`) { + postAllEvents(); +} diff --git a/package.json b/package.json index 8e16c70c..6ddf2f6f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "test:e2e:quickstart": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=chromium tests/e2e/yamcs/", "test:e2e:quickstart:local": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=local-chrome tests/e2e/yamcs/", "test:e2e:watch": "npm test --workspace tests/e2e/opensource -- --ui --config=../playwright-quickstart.config.js", - "wait-for-yamcs": "wait-on http-get://localhost:8090/ -v" + "wait-for-yamcs": "wait-on http-get://localhost:8090/ -v", + "make-example-events": "node ./example/make-example-events.mjs" }, "keywords": [ "openmct", diff --git a/src/const.js b/src/const.js index fb878326..35183d16 100644 --- a/src/const.js +++ b/src/const.js @@ -21,8 +21,11 @@ *****************************************************************************/ export const OBJECT_TYPES = { - COMMANDS_OBJECT_TYPE: 'yamcs.commands', - EVENTS_OBJECT_TYPE: 'yamcs.events', + COMMANDS_ROOT_OBJECT_TYPE: 'yamcs.commands', + COMMANDS_QUEUE_OBJECT_TYPE: 'yamcs.commands.queue', + EVENTS_ROOT_OBJECT_TYPE: 'yamcs.events', + EVENT_SPECIFIC_OBJECT_TYPE: 'yamcs.event.specific', + EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE: 'yamcs.event.specific.severity', TELEMETRY_OBJECT_TYPE: 'yamcs.telemetry', IMAGE_OBJECT_TYPE: 'yamcs.image', STRING_OBJECT_TYPE: 'yamcs.string', diff --git a/src/openmct-yamcs.js b/src/openmct-yamcs.js index 04bb2bc8..b87fefa3 100644 --- a/src/openmct-yamcs.js +++ b/src/openmct-yamcs.js @@ -186,15 +186,33 @@ export default function install( cssClass: 'icon-telemetry' }); - openmct.types.addType(OBJECT_TYPES.EVENTS_OBJECT_TYPE, { + openmct.types.addType(OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, { name: "Events", - description: "To view events", + description: "To view all events", cssClass: "icon-generator-events" }); - openmct.types.addType(OBJECT_TYPES.COMMANDS_OBJECT_TYPE, { + openmct.types.addType(OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, { + name: "Event", + description: "To view events from a specific source", + cssClass: "icon-generator-events" + }); + + openmct.types.addType(OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE, { + name: "Event", + description: "To view events from a specific source with a specific severity or greater", + cssClass: "icon-generator-events" + }); + + openmct.types.addType(OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE, { + name: "Command Queue", + description: "To view command history in a specific queue", + cssClass: "icon-generator-events" // TODO: replace + }); + + openmct.types.addType(OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE, { name: "Commands", - description: "To view command history", + description: "To view the whole command history", cssClass: "icon-generator-events" // TODO: replace }); diff --git a/src/providers/commands.js b/src/providers/commands.js index 0f73ec18..316abc37 100644 --- a/src/providers/commands.js +++ b/src/providers/commands.js @@ -23,27 +23,42 @@ import { OBJECT_TYPES, METADATA_TIME_KEY } from "../const.js"; import { flattenObjectArray } from "../utils.js"; -export function createCommandsObject(openmct, parentKey, namespace) { +export function createRootCommandsObject(openmct, parentKey, namespace) { + const rootCommandsIdentifier = { + key: OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE, + namespace + }; + const rootCommandsObject = createCommandObject(openmct, parentKey, namespace, rootCommandsIdentifier); + + return rootCommandsObject; +} + +export function createCommandObject(openmct, parentKey, namespace, identifier, queueName = null) { + const isRoot = queueName === null; const location = openmct.objects.makeKeyString({ key: parentKey, namespace }); - const identifier = { - key: OBJECT_TYPES.COMMANDS_OBJECT_TYPE, - namespace: namespace - }; + const name = isRoot ? 'Commands' : queueName; + const type = isRoot + ? OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE + : OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE; + const commandObject = { identifier, location, - name: 'Commands', - type: OBJECT_TYPES.COMMANDS_OBJECT_TYPE, + name, + type, telemetry: { values: [ { key: 'commandName', name: 'Command', - format: 'string' + format: 'string', + hints: { + label: 0 + } }, { key: 'utc', @@ -164,9 +179,35 @@ export function createCommandsObject(openmct, parentKey, namespace) { } }; + if (isRoot) { + commandObject.composition = []; + } + return commandObject; } +export async function getCommandQueues(url, instance, processor = 'realtime') { + const commandQueuesURL = `${url}api/processors/${instance}/${processor}/queues`; + const response = await fetch(commandQueuesURL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error(`🛑 Error fetching command queues: ${response.statusText}`); + + return []; + } + + const commandQueueJson = await response.json(); + const { queues } = commandQueueJson; + const queueNames = queues.map(queue => queue.name); + + return queueNames; +} + /** * Convert raw command data from YAMCS to a format which * can be consumed by Open MCT as telemetry. @@ -177,7 +218,7 @@ export function commandToTelemetryDatum(command) { const { generationTime, commandId, attr, assignments, id } = command; const { origin, sequenceNumber, commandName } = commandId; let datum = { - id: OBJECT_TYPES.COMMANDS_OBJECT_TYPE, + id: OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE, generationTime, origin, sequenceNumber, diff --git a/src/providers/event-limit-provider.js b/src/providers/event-limit-provider.js index 3f616cbf..452f9801 100644 --- a/src/providers/event-limit-provider.js +++ b/src/providers/event-limit-provider.js @@ -1,5 +1,7 @@ /* CSS classes for Yamcs parameter monitoring result values. */ +import { OBJECT_TYPES } from "../const"; + const SEVERITY_CSS = { 'WATCH': 'is-event--yellow', 'WARNING': 'is-event--yellow', @@ -65,6 +67,6 @@ export default class EventLimitProvider { } supportsLimits(domainObject) { - return domainObject.type.startsWith('yamcs.events'); + return [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE].includes(domainObject.type); } } diff --git a/src/providers/events.js b/src/providers/events.js index f9482bf8..96b34049 100644 --- a/src/providers/events.js +++ b/src/providers/events.js @@ -21,21 +21,28 @@ *****************************************************************************/ import { OBJECT_TYPES, METADATA_TIME_KEY, SEVERITY_LEVELS } from "../const.js"; -export function createEventsObject(openmct, parentKey, namespace) { +export function createRootEventsObject(openmct, parentKey, namespace) { + const rootEventIdentifier = { + key: OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, + namespace + }; + const rootEventObject = createEventObject(openmct, parentKey, namespace, rootEventIdentifier); + rootEventObject.composition = []; + + return rootEventObject; +} + +export function createEventObject(openmct, parentKey, namespace, identifier, name = 'Events', type = OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE) { const location = openmct.objects.makeKeyString({ key: parentKey, namespace }); - const identifier = { - key: OBJECT_TYPES.EVENTS_OBJECT_TYPE, - namespace - }; - const eventsObject = { + const baseEventObject = { identifier, location, - name: 'Events', - type: OBJECT_TYPES.EVENTS_OBJECT_TYPE, + name, + type, telemetry: { values: [ { @@ -75,7 +82,10 @@ export function createEventsObject(openmct, parentKey, namespace) { { key: 'message', name: 'Message', - format: 'string' + format: 'string', + hints: { + label: 0 + } }, { key: 'type', @@ -96,7 +106,53 @@ export function createEventsObject(openmct, parentKey, namespace) { } }; - return eventsObject; + return baseEventObject; +} + +export function createEventSeverityObjects(openmct, parentEventObject, namespace) { + const childSeverityObjects = []; + for (const severity of SEVERITY_LEVELS) { + const severityIdentifier = { + key: `${parentEventObject.identifier.key}.${severity}`, + namespace + }; + + const severityName = `${parentEventObject.name}: ${severity}`; + + const severityEventObject = createEventObject( + openmct, + parentEventObject.identifier.key, + namespace, + severityIdentifier, + severityName, + OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE + ); + + childSeverityObjects.push(severityEventObject); + } + + return childSeverityObjects; +} + +export async function getEventSources(url, instance) { + const eventSourceURL = `${url}api/archive/${instance}/events/sources`; + const eventSourcesReply = await fetch(eventSourceURL); + if (!eventSourcesReply.ok) { + console.error(`🛑 Failed to fetch event sources: ${eventSourcesReply.statusText}`); + + return []; + } + + const eventSourcesJson = await eventSourcesReply.json(); + + if (eventSourcesJson.sources) { + return eventSourcesJson.sources; + } else if (eventSourcesJson.source) { + // backwards compatibility with older YAMCS versions that only have `source` key + return eventSourcesJson.source; + } else { + return []; + } } export function eventShouldBeFiltered(event, options) { @@ -128,7 +184,7 @@ export function eventToTelemetryDatum(event) { } = event; return { - id: OBJECT_TYPES.EVENTS_OBJECT_TYPE, + id: OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, severity, generationTime, receptionTime, diff --git a/src/providers/historical-telemetry-provider.js b/src/providers/historical-telemetry-provider.js index c08ed6cb..3ed424ac 100644 --- a/src/providers/historical-telemetry-provider.js +++ b/src/providers/historical-telemetry-provider.js @@ -54,8 +54,9 @@ export default class YamcsHistoricalTelemetryProvider { async request(domainObject, options) { options = { ...options }; + const isEvent = ([OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE].includes(domainObject.type)); this.standardizeOptions(options, domainObject); - if ((options.strategy === 'latest') && options.timeContext?.isRealTime()) { + if ((options.strategy === 'latest') && options.timeContext?.isRealTime() && !isEvent) { // Latest requested in realtime, use latest telemetry provider instead const mctDatum = await this.latestTelemetryProvider.requestLatest(domainObject); @@ -66,6 +67,26 @@ export default class YamcsHistoricalTelemetryProvider { const id = domainObject.identifier.key; options.useRawValue = this.hasEnumValue(domainObject); + if ([OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE].includes(domainObject.type)) { + const prefix = `${OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE}.`; + const eventSourceName = domainObject.identifier.key.replace(prefix, ''); + options.eventSource = eventSourceName; + } + + if (domainObject.type === OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE) { + const prefix = `${OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE}.`; + const prefixRemoved = domainObject.identifier.key.replace(prefix, ''); + const [eventSourceName, severity] = prefixRemoved.split('.'); + options.eventSource = eventSourceName; + options.minimumSeverity = severity; + } + + if (domainObject.type === OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE) { + const prefix = `${OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE}.`; + const commandQueueName = domainObject.identifier.key.replace(prefix, ''); + options.commandQueue = commandQueueName; + } + options.isSamples = !this.isImagery(domainObject) && domainObject.type !== OBJECT_TYPES.AGGREGATE_TELEMETRY_TYPE && options.strategy === 'minmax'; @@ -169,6 +190,18 @@ export default class YamcsHistoricalTelemetryProvider { urlWithQueryParameters.searchParams.append('useRawValue', "true"); } + if (options.eventSource) { + urlWithQueryParameters.searchParams.append('source', options.eventSource); + } + + if (options.minimumSeverity) { + urlWithQueryParameters.searchParams.append('severity', options.minimumSeverity); + } + + if (options.commandQueue) { + urlWithQueryParameters.searchParams.append('queue', options.commandQueue); + } + if (options.filters?.severity?.equals?.length) { // add a single minimum severity threshold filter // see https://docs.yamcs.org/yamcs-http-api/events/list-events/ @@ -196,11 +229,21 @@ export default class YamcsHistoricalTelemetryProvider { } getLinkParamsSpecificToId(id) { - if (id === OBJECT_TYPES.EVENTS_OBJECT_TYPE) { + if (id === OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE) { return 'events'; } - if (id === OBJECT_TYPES.COMMANDS_OBJECT_TYPE) { + if (id === OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE) + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE)) { + return 'events'; + } + + if (id === OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE) { + return 'commands'; + } + + if (id.startsWith(OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE)) { return 'commands'; } @@ -208,11 +251,18 @@ export default class YamcsHistoricalTelemetryProvider { } getResponseKeyById(id) { - if (id === OBJECT_TYPES.EVENTS_OBJECT_TYPE) { - return 'event'; + + if (id === (OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE) + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE) + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE)) { + return 'events'; + } + + if (id === OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE) { + return 'entry'; } - if (id === OBJECT_TYPES.COMMANDS_OBJECT_TYPE) { + if (id.startsWith(OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE)) { return 'entry'; } @@ -224,11 +274,13 @@ export default class YamcsHistoricalTelemetryProvider { return []; } - if (id === OBJECT_TYPES.EVENTS_OBJECT_TYPE) { + if (id === OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE) + || id.startsWith(OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE)) { return results.map(event => eventToTelemetryDatum(event)); } - if (id === OBJECT_TYPES.COMMANDS_OBJECT_TYPE) { + if (id === OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE || id.startsWith(OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE)) { return results.map(command => commandToTelemetryDatum(command)); } diff --git a/src/providers/limit-provider.js b/src/providers/limit-provider.js index a05e9490..3277f06c 100644 --- a/src/providers/limit-provider.js +++ b/src/providers/limit-provider.js @@ -93,6 +93,10 @@ export default class LimitProvider { } supportsLimits(domainObject) { + if (domainObject.type.startsWith('yamcs.commands')) { + return false; + } + return domainObject.type.startsWith('yamcs.'); } diff --git a/src/providers/messages.js b/src/providers/messages.js index a0ef8cd8..74ce4f37 100644 --- a/src/providers/messages.js +++ b/src/providers/messages.js @@ -1,8 +1,11 @@ import {OBJECT_TYPES, DATA_TYPES, MDB_TYPE} from '../const.js'; const typeMap = { - [OBJECT_TYPES.COMMANDS_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_COMMANDS, - [OBJECT_TYPES.EVENTS_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, + [OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_COMMANDS, + [OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_COMMANDS, + [OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, + [OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, + [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, [OBJECT_TYPES.TELEMETRY_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, [OBJECT_TYPES.STRING_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, [OBJECT_TYPES.IMAGE_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, @@ -76,7 +79,7 @@ function buildSubscribeMessages() { } function isEventType(type) { - return type === OBJECT_TYPES.EVENTS_OBJECT_TYPE; + return [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE].includes(type); } function isAlarmType(type) { @@ -85,7 +88,7 @@ function isAlarmType(type) { } function isCommandType(type) { - return type === OBJECT_TYPES.COMMANDS_OBJECT_TYPE; + return type === OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE || type === OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE; } function isMdbChangesType(type) { diff --git a/src/providers/object-provider.js b/src/providers/object-provider.js index 6dca9e9b..2140e06a 100644 --- a/src/providers/object-provider.js +++ b/src/providers/object-provider.js @@ -28,8 +28,8 @@ import { } from '../utils.js'; import { OBJECT_TYPES, NAMESPACE } from '../const.js'; -import { createCommandsObject } from './commands.js'; -import { createEventsObject } from './events.js'; +import { createCommandObject, getCommandQueues, createRootCommandsObject } from './commands.js'; +import { createRootEventsObject, createEventObject, getEventSources, createEventSeverityObjects } from './events.js'; import { getPossibleStatusesFromParameter, getRoleFromParameter, isOperatorStatusParameter } from './user/operator-status-parameter.js'; import { getMissionActionFromParameter, getPossibleMissionActionStatusesFromParameter, isMissionStatusParameter } from './mission-status/mission-status-parameter.js'; @@ -63,15 +63,6 @@ export default class YamcsObjectProvider { #initialize() { this.#createRootObject(); - const eventsObject = createEventsObject(this.openmct, this.key, this.namespace); - const commandsObject = createCommandsObject(this.openmct, this.key, this.namespace); - this.#addObject(commandsObject); - this.#addObject(eventsObject); - this.rootObject.composition.push( - eventsObject.identifier, - commandsObject.identifier - ); - this.openmct.on('destroy', this.#unsubscribeFromAll); } @@ -222,9 +213,65 @@ export default class YamcsObjectProvider { this.#addParameterObject(parameter); }); + await this.#createCommands(); + + await this.#createEvents(); + return this.dictionary; } + async #createCommands() { + const rootCommandsObject = createRootCommandsObject(this.openmct, this.key, this.namespace); + this.#addObject(rootCommandsObject); + this.rootObject.composition.push( + rootCommandsObject.identifier + ); + const commandQueues = await getCommandQueues(this.url, this.instance); + commandQueues.forEach(commandQueueName => { + const queueKey = qualifiedNameToId(`${OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE}.${commandQueueName}`); + const queueIdentifier = { + key: queueKey, + namespace: this.namespace + }; + const commandQueueObject = createCommandObject(this.openmct, rootCommandsObject.identifier.key, this.namespace, queueIdentifier, commandQueueName, OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE); + + this.#addObject(commandQueueObject); + + rootCommandsObject.composition.push(commandQueueObject.identifier); + }); + } + + async #createEvents() { + const rootEventsObject = createRootEventsObject(this.openmct, this.key, this.namespace); + this.#addObject(rootEventsObject); + this.rootObject.composition.push(rootEventsObject.identifier); + + // Fetch child event names + const eventSourceNames = await getEventSources(this.url, this.instance); + eventSourceNames.forEach(eventSourceName => { + const childEventKey = qualifiedNameToId(`${OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE}.${eventSourceName}`); + const childEventIdentifier = { + key: childEventKey, + namespace: this.namespace + }; + const childEventObject = createEventObject(this.openmct, rootEventsObject.identifier.key, this.namespace, childEventIdentifier, eventSourceName, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE); + + this.#addObject(childEventObject); + + const childSeverityObjects = createEventSeverityObjects(this.openmct, childEventObject, this.namespace); + childSeverityObjects.forEach(severityObject => { + this.#addObject(severityObject); + if (!childEventObject.composition) { + childEventObject.composition = []; + } + + childEventObject.composition.push(severityObject.identifier); + }); + + rootEventsObject.composition.push(childEventObject.identifier); + }); + } + #getMdbUrl(operation, name = '') { return this.url + 'api/mdb/' + this.instance + '/' + operation + name; } diff --git a/tests/e2e/yamcs/timeline.e2e.spec.mjs b/tests/e2e/yamcs/timeline.e2e.spec.mjs new file mode 100644 index 00000000..62664f83 --- /dev/null +++ b/tests/e2e/yamcs/timeline.e2e.spec.mjs @@ -0,0 +1,90 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +import { pluginFixtures, appActions } from 'openmct-e2e'; +import { postAllEvents } from '../../../example/make-example-events.mjs'; // Updated path and extension +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults, setStartOffset, setEndOffset, setFixedTimeMode } = appActions; + +test.describe("Timeline Events in @yamcs", () => { + test('Can create a timeline with YAMCS events', async ({ page }) => { + // Go to baseURL + await page.goto("./", { waitUntil: "networkidle" }); + await page.getByLabel('Expand myproject folder').click(); + const eventsTreeItem = page.getByRole('treeitem', { name: /Events/ }); + const eventTimelineView = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); + const objectPane = page.getByLabel(`${eventTimelineView.name} Object View`); + await eventsTreeItem.dragTo(objectPane); + await postAllEvents(); + + await setStartOffset(page, { startMins: '02' }); + await setEndOffset(page, { endMins: '02' }); + await setFixedTimeMode(page); + + await page + .getByLabel(eventTimelineView.name) + .getByLabel(/Pressure threshold exceeded/) + .first() + .click(); + await page.getByRole('tab', { name: 'Event' }).click(); + + // ensure the event inspector has the the same event + await expect(page.getByText(/Pressure threshold exceeded/)).toBeVisible(); + + await page.getByLabel('Expand Events yamcs.events').click(); + await page.getByLabel('Expand PressureModule yamcs.').click(); + const pressureModuleInfoTreeItem = page.getByRole('treeitem', { name: /PressureModule: info/ }); + await pressureModuleInfoTreeItem.dragTo(objectPane); + + const pressureModuleCriticalTreeItem = page.getByRole('treeitem', { name: /PressureModule: critical/ }); + await pressureModuleCriticalTreeItem.dragTo(objectPane); + + // click on the event inspector tab + await page.getByRole('tab', { name: 'Event' }).click(); + + await expect(page.getByLabel('PressureModule: info Object').getByLabel(/Pressure system check completed/).first()).toBeVisible(); + await page.getByLabel('PressureModule: info Object').getByLabel(/Pressure system check completed/).first().click(); + // ensure the tooltip shows up + await expect( + page.getByRole('tooltip').getByText(/Pressure system check completed/) + ).toBeVisible(); + + // and that event appears in the inspector + await expect( + page.getByLabel('Inspector Views').getByText(/Pressure system check completed/) + ).toBeVisible(); + + // info statements should be hidden in critical severity + await expect(page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure system check/).first()).toBeHidden(); + await expect(page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure threshold exceeded/).first()).toBeVisible(); + await page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure threshold exceeded/).first().click(); + await expect(page.getByLabel('Inspector Views').getByText('Pressure threshold exceeded')).toBeVisible(); + await expect( + page.getByRole('tooltip').getByText(/Pressure threshold exceeded/) + ).toBeVisible(); + + // turn on extended lines + await page.getByLabel('Toggle extended event lines overlay for PressureModule: critical').click(); + const overlayLinesContainer = page.locator('.c-timeline__overlay-lines'); + await expect(overlayLinesContainer.locator('.c-timeline__event-line--extended').last()).toBeVisible(); + }); +});