diff --git a/src/Frontend/src/components/MaximizableCodeEditor.vue b/src/Frontend/src/components/MaximizableCodeEditor.vue new file mode 100644 index 000000000..57ffa635b --- /dev/null +++ b/src/Frontend/src/components/MaximizableCodeEditor.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/src/Frontend/src/components/messages2/DiffViewer.vue b/src/Frontend/src/components/messages2/DiffViewer.vue index 5deb14fd6..3048c4e3a 100644 --- a/src/Frontend/src/components/messages2/DiffViewer.vue +++ b/src/Frontend/src/components/messages2/DiffViewer.vue @@ -166,8 +166,8 @@ onBeforeUnmount(() => { .maximize-modal-content { background-color: white; - width: calc(100% - 40px); - height: calc(100% - 40px); + width: 95vw; + height: 90vh; border-radius: 4px; overflow: hidden; display: flex; @@ -186,7 +186,7 @@ onBeforeUnmount(() => { .maximize-modal-title { font-weight: bold; - font-size: 16px; + font-size: 1rem; } .maximize-modal-close { diff --git a/src/Frontend/src/components/messages2/SagaDiagram.spec.ts b/src/Frontend/src/components/messages2/SagaDiagram.spec.ts index 0a0fe4d64..996d9c9f9 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram.spec.ts +++ b/src/Frontend/src/components/messages2/SagaDiagram.spec.ts @@ -257,12 +257,16 @@ function rendercomponent({ initialState = {} }: { initialState?: { MessageStore? CodeEditor: true, CopyToClipboard: true, }, + directives: { + // Add stub for tippy directive + tippy: () => {}, + }, }, }); const dslAPI: componentDSL = { action1: () => { - // Add actions here;dl;;lksd;lksd;lkdmdslm,.mc,. + // Add actions here; }, assert: { NoSagaDataAvailableMessageIsShownWithMessage(message: RegExp) { diff --git a/src/Frontend/src/components/messages2/SagaDiagram/MessageDataBox.vue b/src/Frontend/src/components/messages2/SagaDiagram/MessageDataBox.vue index 0da3460a6..802898d46 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/MessageDataBox.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/MessageDataBox.vue @@ -1,27 +1,43 @@ @@ -30,6 +46,10 @@ const { messageDataLoading } = storeToRefs(sagaDiagramStore); display: flex; } +.message-data-box-content { + display: block; +} + .message-data-box-text { display: inline-block; margin-right: 0.25rem; @@ -48,6 +68,24 @@ const { messageDataLoading } = storeToRefs(sagaDiagramStore); display: inline-block; width: 100%; text-align: center; + color: #666; + font-style: italic; +} + +.message-data-box-text--error { + display: inline-block; + width: 100%; + text-align: center; + color: #a94442; + font-style: italic; +} + +.message-data-box-text--unsupported { + display: inline-block; + width: 100%; + text-align: center; + color: #8a6d3b; + font-style: italic; } .message-data-loading { @@ -55,4 +93,28 @@ const { messageDataLoading } = storeToRefs(sagaDiagramStore); justify-content: center; align-items: center; } + +.message-data-box-error { + padding: 1rem; + justify-content: center; +} +.message-data-box-content :deep(.wrapper.maximazable-code-editor--inline-instance) { + border: none; + border-radius: 0; + margin-top: 0; + font-size: 0.75rem; +} + +.message-data-box-content :deep(.wrapper.maximazable-code-editor--inline-instance .toolbar) { + border: none; + border-radius: 0; + background-color: transparent; + padding: 0; + margin-bottom: 0; +} + +.message-data-box-content :deep(.wrapper.maximazable-code-editor--inline-instance .cm-editor) { + /* Override any borders from the default theme */ + border: none; +} diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts b/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts index 910f30e9d..437c4a8b3 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts @@ -1,22 +1,22 @@ import { SagaHistory } from "@/resources/SagaHistory"; import { typeToName } from "@/composables/typeHumanizer"; -import { SagaMessageData, SagaMessageDataItem } from "@/stores/SagaDiagramStore"; +import { SagaMessageData } from "@/stores/SagaDiagramStore"; import { getTimeoutFriendly } from "@/composables/deliveryDelayParser"; export interface SagaMessageViewModel { MessageId: string; - MessageFriendlyTypeName: string; + FriendlyTypeName: string; FormattedTimeSent: string; - Data: SagaMessageDataItem[]; + Data: SagaMessageData; IsEventMessage: boolean; IsCommandMessage: boolean; } export interface InitiatingMessageViewModel { - MessageType: string; + FriendlyTypeName: string; IsSagaTimeoutMessage: boolean; FormattedMessageTimestamp: string; IsEventMessage: boolean; - MessageData: SagaMessageDataItem[]; + MessageData: SagaMessageData; HasRelatedTimeoutRequest?: boolean; MessageId: string; } @@ -78,7 +78,7 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: const initiatingMessageTimestamp = new Date(update.initiating_message?.time_sent || Date.now()); // Find message data for initiating message - const initiatingMessageData = update.initiating_message ? messagesData.find((m) => m.message_id === update.initiating_message.message_id)?.data || [] : []; + const initiatingMessageData = update.initiating_message ? findMessageData(messagesData, update.initiating_message.message_id) : createEmptyMessageData(); // Create common base message objects with shared properties const outgoingMessages = update.outgoing_messages.map((msg) => { @@ -89,7 +89,7 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: const isEventMessage = msg.intent === "Publish"; // Find corresponding message data - const messageData = messagesData.find((m) => m.message_id === msg.message_id)?.data || []; + const messageData = findMessageData(messagesData, msg.message_id); return { MessageType: msg.message_type || "", MessageId: msg.message_id, @@ -97,7 +97,7 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: HasTimeout: hasTimeout, TimeoutSeconds: timeoutSeconds, TimeoutFriendly: getTimeoutFriendly(delivery_delay), - MessageFriendlyTypeName: typeToName(msg.message_type || ""), + FriendlyTypeName: typeToName(msg.message_type || ""), Data: messageData, IsEventMessage: isEventMessage, IsCommandMessage: !isEventMessage, @@ -132,8 +132,8 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: Status: update.status, StatusDisplay: update.status === "new" ? "Saga Initiated" : "Saga Updated", InitiatingMessage: { + FriendlyTypeName: typeToName(update.initiating_message?.message_type || "Unknown Message") || "", MessageId: update.initiating_message?.message_id || "", - MessageType: typeToName(update.initiating_message?.message_type || "Unknown Message") || "", FormattedMessageTimestamp: `${initiatingMessageTimestamp.toLocaleDateString()} ${initiatingMessageTimestamp.toLocaleTimeString()}`, MessageData: initiatingMessageData, IsEventMessage: update.initiating_message?.intent === "Publish", @@ -160,3 +160,22 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: return updates; } + +// Helper function to find message data or create empty data if not found +function findMessageData(messagesData: SagaMessageData[], messageId: string): SagaMessageData { + const messageData = messagesData.find((m) => m.message_id === messageId); + return messageData || createEmptyMessageData(); +} + +// Helper function to create an empty message data object +function createEmptyMessageData(): SagaMessageData { + return { + message_id: "", + body: { + data: {}, + loading: false, + failed_to_load: false, + not_found: false, + }, + }; +} diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingMessage.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingMessage.vue index ec4766a4c..65ba33c9f 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingMessage.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingMessage.vue @@ -27,11 +27,11 @@ const props = defineProps<{ }" > -

{{ message.MessageFriendlyTypeName }}

+

{{ message.FriendlyTypeName }}

{{ message.FormattedTimeSent }}
- +
diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue index 0f5658924..041287824 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue @@ -46,7 +46,7 @@ watch(
@@ -66,11 +66,11 @@ watch( }" > -

{{ message.MessageFriendlyTypeName }}

+

{{ message.FriendlyTypeName }}

{{ message.FormattedTimeSent }}
- +
diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue index 8cbb54675..32c3a4872 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue @@ -4,7 +4,7 @@ import MessageDataBox from "./MessageDataBox.vue"; import SagaOutgoingTimeoutMessage from "./SagaOutgoingTimeoutMessage.vue"; import SagaOutgoingMessage from "./SagaOutgoingMessage.vue"; import DiffViewer from "@/components/messages2/DiffViewer.vue"; -import CodeEditor from "@/components/CodeEditor.vue"; +import MaximizableCodeEditor from "@/components/MaximizableCodeEditor.vue"; import { useSagaDiagramStore } from "@/stores/SagaDiagramStore"; import { ref, watch, computed } from "vue"; import { EditorView } from "@codemirror/view"; @@ -18,30 +18,18 @@ import TimeoutIcon from "@/assets/timeout.svg"; import EventIcon from "@/assets/event.svg"; import SagaTimeoutIcon from "@/assets/SagaTimeoutIcon.svg"; -// Define the monospace theme for CodeEditor +// Define monospace theme with specific selectors for this component const monospaceTheme = EditorView.baseTheme({ - "&": { + ".maximazable-code-editor--inline-instance .cm-editor": { fontFamily: "monospace", fontSize: "0.75rem", backgroundColor: "#f2f2f2", }, - ".cm-editor": { - fontFamily: "monospace", - fontSize: "0.75rem", - backgroundColor: "#f2f2f2", - }, - ".cm-scroller": { + ".maximazable-code-editor--inline-instance .cm-scroller": { backgroundColor: "#f2f2f2", }, }); -// Define types for JSON values and properties -type JsonValue = string | number | boolean | null | JsonObject | JsonArray; -interface JsonObject { - [key: string]: JsonValue; -} -type JsonArray = Array; - const props = defineProps<{ update: SagaUpdateViewModel; showMessageData?: boolean; @@ -163,7 +151,7 @@ const hasStateChanges = computed(() => { :data-message-id="update.InitiatingMessage.IsSagaTimeoutMessage ? update.MessageId : ''" > -

{{ update.InitiatingMessage.MessageType }}

+

{{ update.InitiatingMessage.FriendlyTypeName }}

{{ update.InitiatingMessage.FormattedMessageTimestamp }}
@@ -171,7 +159,16 @@ const hasStateChanges = computed(() => {
@@ -188,7 +185,7 @@ const hasStateChanges = computed(() => {
- +
@@ -206,7 +203,7 @@ const hasStateChanges = computed(() => {
- +
@@ -381,7 +378,6 @@ const hasStateChanges = computed(() => { padding: 0.2rem; background-color: #ffffff; border: solid 1px #cccccc; - font-size: 0.75rem; } .message-data--active { @@ -435,12 +431,24 @@ const hasStateChanges = computed(() => { } /* Override CodeEditor wrapper styles */ -.json-container :deep(.wrapper) { +.json-container :deep(.wrapper.maximazable-code-editor--inline-instance) { border-radius: 0; border: none; background-color: #f2f2f2; margin-top: 0; + font-size: 0.75rem; } +.json-container :deep(.wrapper.maximazable-code-editor--inline-instance .toolbar) { + border: none; + border-radius: 0; + background-color: transparent; + padding: 0; + margin-bottom: 0; +} + +/* :deep(.maximazable-code-editor--inline-instance .cm-scroller) { + background-color: #f2f2f2; +} */ .no-changes-message { padding: 1rem; diff --git a/src/Frontend/src/stores/DataContainer.ts b/src/Frontend/src/stores/DataContainer.ts new file mode 100644 index 000000000..80a0983e8 --- /dev/null +++ b/src/Frontend/src/stores/DataContainer.ts @@ -0,0 +1,10 @@ +/** + * A container for data with loading states. + * Used to track loading, error, and not found states for data fetched from APIs. + */ +export interface DataContainer { + loading?: boolean; + failed_to_load?: boolean; + not_found?: boolean; + data: T; +} diff --git a/src/Frontend/src/stores/MessageStore.ts b/src/Frontend/src/stores/MessageStore.ts index 808e5a935..c3d1ee19d 100644 --- a/src/Frontend/src/stores/MessageStore.ts +++ b/src/Frontend/src/stores/MessageStore.ts @@ -11,13 +11,7 @@ import moment from "moment/moment"; import { parse, stringify } from "lossless-json"; import xmlFormat from "xml-formatter"; import { useArchiveMessage, useRetryMessages, useUnarchiveMessage } from "@/composables/serviceFailedMessage"; - -interface DataContainer { - loading?: boolean; - failed_to_load?: boolean; - not_found?: boolean; - data: T; -} +import { DataContainer } from "./DataContainer"; interface Model { id?: string; diff --git a/src/Frontend/src/stores/SagaDiagramStore.ts b/src/Frontend/src/stores/SagaDiagramStore.ts index 6cf02209b..bbd06884b 100644 --- a/src/Frontend/src/stores/SagaDiagramStore.ts +++ b/src/Frontend/src/stores/SagaDiagramStore.ts @@ -3,17 +3,14 @@ import { ref, watch } from "vue"; import { SagaHistory, SagaMessage } from "@/resources/SagaHistory"; import { useFetchFromServiceControl } from "@/composables/serviceServiceControlUrls"; import Message from "@/resources/Message"; -import { parse } from "lossless-json"; -import { useMessageStore } from "@/stores/MessageStore"; +import { parse, stringify } from "lossless-json"; +import xmlFormat from "xml-formatter"; +import { DataContainer } from "./DataContainer"; +import { useMessageStore } from "./MessageStore"; -const StandardKeys = ["$type", "Id", "Originator", "OriginalMessageId"]; -export interface SagaMessageDataItem { - key: string; - value: string; -} export interface SagaMessageData { message_id: string; - data: SagaMessageDataItem[]; + body: DataContainer<{ value?: string; content_type?: string }>; } export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => { const sagaHistory = ref(null); @@ -28,11 +25,8 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => { const scrollToTimeoutRequest = ref(false); const scrollToTimeout = ref(false); const MessageBodyEndpoint = "messages/{0}/body"; - - // Get message store to watch for changes const messageStore = useMessageStore(); - // Watch for message_id changes in the MessageStore and update selectedMessageId watch( () => messageStore.state.data.message_id, (newMessageId) => { @@ -90,45 +84,55 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => { } } - function createEmptyMessageData(message_id: string): SagaMessageData { - return { - message_id, - data: [], - }; - } - async function fetchSagaMessageData(message: SagaMessage): Promise { const bodyUrl = (message.body_url ?? formatUrl(MessageBodyEndpoint, message.message_id)).replace(/^\//, ""); + const result: SagaMessageData = { + message_id: message.message_id, + body: { data: {} }, + }; + + result.body.loading = true; + result.body.failed_to_load = false; try { - const response = await useFetchFromServiceControl(bodyUrl, { cache: "no-store" }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const response = await useFetchFromServiceControl(bodyUrl); + if (response.status === 404) { + result.body.not_found = true; + return result; } - const body = await response.json(); - if (!body) { - return createEmptyMessageData(message.message_id); + const contentType = response.headers.get("content-type"); + result.body.data.content_type = contentType ?? "text/plain"; + result.body.data.value = await response.text(); + + if (contentType === "application/json" && result.body.data.value) { + // Only format non-empty JSON objects + result.body.data.value = result.body.data.value !== "{}" ? (stringify(parse(result.body.data.value), null, 2) ?? result.body.data.value) : ""; + } else if (contentType === "text/xml" && result.body.data.value) { + // Format XML if it has content in the root element + const xmlRootElement = getContentOfXmlRootElement(result.body.data.value); + result.body.data.value = xmlRootElement ? xmlFormat(result.body.data.value, { indentation: " ", collapseContent: true }) : ""; } + } catch { + result.body.failed_to_load = true; + } finally { + result.body.loading = false; + } - let data: SagaMessageDataItem[]; - if (typeof body === "string" && body.trim().startsWith(" { } } - function getXmlData(xmlString: string): SagaMessageDataItem[] { - try { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(xmlString, "application/xml"); - - // Get the root element - const rootElement = xmlDoc.documentElement; - if (!rootElement) { - return []; - } - - // Handle both v5 and pre-v5 message formats - const messageRoot = rootElement.nodeName === "Messages" ? Array.from(rootElement.children)[0] : rootElement; - - if (!messageRoot) { - return []; - } - - // Convert child elements to SagaMessageDataItems - return Array.from(messageRoot.children).map((node) => ({ - key: node.nodeName, - value: node.textContent?.trim() || "", - })); - } catch (error) { - console.error("Error parsing message data:", error); - return []; - } - } - - function processJsonValues(jsonBody: string | Record): SagaMessageDataItem[] { - let parsedBody: Record; - if (typeof jsonBody === "string") { - try { - parsedBody = parse(jsonBody) as Record; - } catch (e) { - console.error("Error parsing JSON:", e); - return []; - } - } else { - parsedBody = jsonBody; - } - - const items: SagaMessageDataItem[] = []; - - for (const key in parsedBody) { - if (!StandardKeys.includes(key)) { - items.push({ - key: key, - value: String(parsedBody[key] ?? ""), - }); - } - } - - return items; - } - function clearSagaHistory() { sagaHistory.value = null; sagaId.value = null;