From dd03a8c1a11d99e24993641de7d87f1eb49fb110 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 29 Apr 2025 14:41:12 -0600 Subject: [PATCH 01/15] animation vendor-specific styles for old browsers --- .../messages2/SagaDiagram/SagaUpdateNode.vue | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue index f4947437d..f42a93e8c 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue @@ -393,6 +393,51 @@ watch( } } +@keyframes blink-border { + 0%, + 100% { + border-color: #000000; + } + 20%, + 60% { + border-color: #cccccc; + } + 40%, + 80% { + border-color: #000000; + } +} + +@-moz-keyframes blink-border { + 0%, + 100% { + border-color: #000000; + } + 20%, + 60% { + border-color: #cccccc; + } + 40%, + 80% { + border-color: #000000; + } +} + +@-o-keyframes blink-border { + 0%, + 100% { + border-color: #000000; + } + 20%, + 60% { + border-color: #cccccc; + } + 40%, + 80% { + border-color: #000000; + } +} + @keyframes blink-border { 0%, 100% { From c822b25205c87457e27de3ddd177540907ca3c74 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 29 Apr 2025 15:44:04 -0600 Subject: [PATCH 02/15] selected border color --- .../src/components/messages2/SagaDiagram/SagaUpdateNode.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue index f42a93e8c..8a0639292 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue @@ -396,7 +396,7 @@ watch( @keyframes blink-border { 0%, 100% { - border-color: #000000; + border-color: #00a3c4; } 20%, 60% { @@ -404,7 +404,7 @@ watch( } 40%, 80% { - border-color: #000000; + border-color: #00a3c4; } } From d9350c63e89351dd5d60301c183c4ded3f850cc6 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 30 Apr 2025 15:42:13 -0600 Subject: [PATCH 03/15] Saga Update properties --- .../SagaDiagram/SagaDiagramParser.ts | 22 ++++- .../messages2/SagaDiagram/SagaUpdateNode.vue | 88 +++++++++++++++---- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts b/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts index d08d0917f..34931aa50 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts @@ -36,6 +36,9 @@ export interface SagaUpdateViewModel { OutgoingTimeoutMessages: SagaTimeoutMessageViewModel[]; HasOutgoingMessages: boolean; HasOutgoingTimeoutMessages: boolean; + showUpdatedPropertiesOnly: boolean; + stateAfterChange: Record; + previousStateAfterChange?: Record; } export interface SagaViewModel { @@ -54,12 +57,20 @@ export interface SagaViewModel { export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: SagaMessageData[]): SagaUpdateViewModel[] { if (!sagaHistory || !sagaHistory.changes || !sagaHistory.changes.length) return []; - return sagaHistory.changes + const updates = sagaHistory.changes .map((update) => { const startTime = new Date(update.start_time); const finishTime = new Date(update.finish_time); const initiatingMessageTimestamp = new Date(update.initiating_message?.time_sent || Date.now()); + // Parse the state_after_change JSON + let stateAfterChange: Record = {}; + try { + stateAfterChange = JSON.parse(update.state_after_change || "{}"); + } catch (e) { + console.error("Error parsing state_after_change:", e); + } + // Find message data for initiating message const initiatingMessageData = update.initiating_message ? messagesData.find((m) => m.message_id === update.initiating_message.message_id)?.data || [] : []; @@ -123,8 +134,17 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: OutgoingMessages: regularMessages, HasOutgoingMessages: regularMessages.length > 0, HasOutgoingTimeoutMessages: outgoingTimeoutMessages.length > 0, + showUpdatedPropertiesOnly: true, // Default to showing only updated properties + stateAfterChange: stateAfterChange, }; }) .sort((a, b) => a.StartTime.getTime() - b.StartTime.getTime()) .sort((a, b) => a.FinishTime.getTime() - b.FinishTime.getTime()); + + // Add reference to previous state for each update except the first one + for (let i = 1; i < updates.length; i++) { + updates[i].previousStateAfterChange = updates[i - 1].stateAfterChange; + } + + return updates; } diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue index 8a0639292..7a0ee8983 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 { useSagaDiagramStore } from "@/stores/SagaDiagramStore"; -import { ref, watch } from "vue"; +import { ref, watch, computed } from "vue"; // Import the images directly import CommandIcon from "@/assets/command.svg"; @@ -21,6 +21,7 @@ const props = defineProps<{ const store = useSagaDiagramStore(); const initiatingMessageRef = ref(null); const isActive = ref(false); +const showAllProperties = ref(!props.update.showUpdatedPropertiesOnly); // Watch for changes to selectedMessageId watch( @@ -41,6 +42,63 @@ watch( } } ); + +// Function to toggle between showing all properties and only updated properties +const togglePropertyView = (event: Event, showAll: boolean) => { + event.preventDefault(); + showAllProperties.value = showAll; + // Instead of directly modifying the prop, we update our local view state + // props.update.showUpdatedPropertiesOnly = !showAll; +}; + +// Compute the properties to display based on the current state +const displayProperties = computed(() => { + const properties: { name: string; value: string; isNew: boolean }[] = []; + const state = props.update.stateAfterChange; + const previousState = props.update.previousStateAfterChange || {}; + const isFirstNode = props.update.IsFirstNode; + + // Function to check if a property has changed + const hasPropertyChanged = (key: string) => { + if (isFirstNode) return true; // For the first node, all properties are "new" + return JSON.stringify(state[key]) !== JSON.stringify(previousState[key]); + }; + + // Function to format value differences + const formatValue = (key: string) => { + const currentValue = state[key]; + if (isFirstNode || !props.update.previousStateAfterChange) { + return String(currentValue); + } + + const prevValue = previousState[key]; + if (JSON.stringify(currentValue) !== JSON.stringify(prevValue)) { + return `${prevValue} → ${currentValue}`; + } + return String(currentValue); + }; + + // Filter out standard keys like $type, Id, Originator, OriginalMessageId + const standardKeys = ["$type", "Id", "Originator", "OriginalMessageId"]; + + // Add all properties that should be displayed + for (const key in state) { + if (standardKeys.includes(key)) continue; + + const propertyChanged = hasPropertyChanged(key); + + // Skip unchanged properties when showing only updated properties + if (!showAllProperties.value && !propertyChanged && !isFirstNode) continue; + + properties.push({ + name: isFirstNode ? `${key} (new)` : key, // Add "(new)" suffix for first node + value: formatValue(key), + isNew: isFirstNode || propertyChanged, + }); + } + + return properties; +}); diff --git a/src/Frontend/src/components/messages2/SagaDiagram.vue b/src/Frontend/src/components/messages2/SagaDiagram.vue index bc2144a0d..3091a7e22 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram.vue @@ -133,8 +133,8 @@ const vm = computed(() => { } .container { - width: 66.6667%; min-width: 50rem; + max-width: 100rem; } .loading-container { diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue index eced9b366..14170588d 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue @@ -35,7 +35,7 @@ const navigateToTimeout = () => {
-
+

{{ message.MessageFriendlyTypeName }}

{{ message.FormattedTimeSent }}
diff --git a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue index 7a0ee8983..7308be66a 100644 --- a/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue +++ b/src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue @@ -3,6 +3,7 @@ import { SagaUpdateViewModel } from "./SagaDiagramParser"; import MessageDataBox from "./MessageDataBox.vue"; import SagaOutgoingTimeoutMessage from "./SagaOutgoingTimeoutMessage.vue"; import SagaOutgoingMessage from "./SagaOutgoingMessage.vue"; +import DiffViewer from "@/components/messages2/DiffViewerV2.vue"; import { useSagaDiagramStore } from "@/stores/SagaDiagramStore"; import { ref, watch, computed } from "vue"; @@ -13,6 +14,13 @@ import SagaUpdatedIcon from "@/assets/SagaUpdatedIcon.svg"; import TimeoutIcon from "@/assets/timeout.svg"; import SagaTimeoutIcon from "@/assets/SagaTimeoutIcon.svg"; +// 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; @@ -21,7 +29,6 @@ const props = defineProps<{ const store = useSagaDiagramStore(); const initiatingMessageRef = ref(null); const isActive = ref(false); -const showAllProperties = ref(!props.update.showUpdatedPropertiesOnly); // Watch for changes to selectedMessageId watch( @@ -43,61 +50,77 @@ watch( } ); -// Function to toggle between showing all properties and only updated properties -const togglePropertyView = (event: Event, showAll: boolean) => { - event.preventDefault(); - showAllProperties.value = showAll; - // Instead of directly modifying the prop, we update our local view state - // props.update.showUpdatedPropertiesOnly = !showAll; +// Format a JSON value for display +const formatJsonValue = (value: unknown): string => { + if (value === null || value === undefined) return "null"; + if (typeof value === "object") { + return JSON.stringify(value, null, 2); + } + return String(value); }; -// Compute the properties to display based on the current state -const displayProperties = computed(() => { - const properties: { name: string; value: string; isNew: boolean }[] = []; - const state = props.update.stateAfterChange; - const previousState = props.update.previousStateAfterChange || {}; - const isFirstNode = props.update.IsFirstNode; +// Process JSON state and remove standard properties +const processState = (state: unknown): Record => { + if (!state) return {}; - // Function to check if a property has changed - const hasPropertyChanged = (key: string) => { - if (isFirstNode) return true; // For the first node, all properties are "new" - return JSON.stringify(state[key]) !== JSON.stringify(previousState[key]); - }; + let stateObj: Record; + try { + stateObj = typeof state === "string" ? JSON.parse(state) : (state as Record); + } catch (e) { + console.error("Error parsing state:", e); + return {}; + } - // Function to format value differences - const formatValue = (key: string) => { - const currentValue = state[key]; - if (isFirstNode || !props.update.previousStateAfterChange) { - return String(currentValue); - } + // Filter out standard properties + const standardKeys = ["$type", "Id", "Originator", "OriginalMessageId"]; + const filteredState: Record = {}; - const prevValue = previousState[key]; - if (JSON.stringify(currentValue) !== JSON.stringify(prevValue)) { - return `${prevValue} → ${currentValue}`; + Object.keys(stateObj).forEach((key) => { + if (!standardKeys.includes(key)) { + // Type assertion here since we can't guarantee the type exactly matches JsonValue + filteredState[key] = stateObj[key] as JsonValue; } - return String(currentValue); - }; + }); - // Filter out standard keys like $type, Id, Originator, OriginalMessageId - const standardKeys = ["$type", "Id", "Originator", "OriginalMessageId"]; + return filteredState; +}; - // Add all properties that should be displayed - for (const key in state) { - if (standardKeys.includes(key)) continue; +// Compute the diff between current and previous states +const stateDiff = computed(() => { + const currentState = processState(props.update.stateAfterChange); + const previousState = processState(props.update.previousStateAfterChange); + const isFirstNode = props.update.IsFirstNode; - const propertyChanged = hasPropertyChanged(key); + // Format the current state + const currentFormatted = formatJsonValue(currentState); + + // If it's the first node, just return the current state + if (isFirstNode) { + return { + formattedState: currentFormatted, + // Provide default empty strings for diff view to prevent type errors + previousFormatted: "", + currentFormatted: currentFormatted, + }; + } - // Skip unchanged properties when showing only updated properties - if (!showAllProperties.value && !propertyChanged && !isFirstNode) continue; + // Format the JSON state objects + const previousFormatted = formatJsonValue(previousState); - properties.push({ - name: isFirstNode ? `${key} (new)` : key, // Add "(new)" suffix for first node - value: formatValue(key), - isNew: isFirstNode || propertyChanged, - }); - } + return { + previousFormatted, + currentFormatted, + }; +}); + +// Determine if there are changes to display +const hasStateChanges = computed(() => { + if (props.update.IsFirstNode) return true; + + const currentState = processState(props.update.stateAfterChange); + const previousState = processState(props.update.previousStateAfterChange); - return properties; + return JSON.stringify(currentState) !== JSON.stringify(previousState); }); @@ -147,20 +170,25 @@ const displayProperties = computed(() => {
-
- All Properties / - Updated Properties +
+

Initial Saga State

+

State Changes

+ + +
+
{{ stateDiff.formattedState }}
+
+ + +
+
No state changes in this update
+
+ + +
+ +
- - -
    -
  • - {{ prop.name }} - = - {{ prop.value }} -
  • -
-
No properties to display
@@ -313,54 +341,6 @@ const displayProperties = computed(() => { font-size: 0.8rem; } -.saga-properties { - margin: 0 -0.25rem; - padding: 0.25rem; - font-size: 0.6rem; - text-transform: uppercase; -} - -.saga-properties-link { - padding: 0 0.25rem; - text-decoration: underline; -} - -.saga-properties-link--active { - font-weight: 900; - color: #000000; -} - -.saga-properties-list { - margin: 0; - padding-left: 0.25rem; - list-style: none; -} - -.saga-properties-list-item { - display: flex; -} - -.saga-properties-list-text { - display: inline-block; - padding-top: 0.25rem; - padding-right: 0.75rem; - overflow: hidden; - font-size: 0.75rem; - white-space: nowrap; -} - -.saga-properties-list-text:first-child { - min-width: 8rem; - max-width: 8rem; - display: inline-block; - text-overflow: ellipsis; -} - -.saga-properties-list-text:last-child { - padding-right: 0; - text-overflow: ellipsis; -} - .message-data { display: none; padding: 0.2rem; @@ -373,13 +353,6 @@ const displayProperties = computed(() => { display: block; } -.no-properties { - padding: 0.25rem; - font-style: italic; - font-size: 0.8rem; - color: #666; -} - .saga-icon { display: block; float: left; @@ -399,6 +372,7 @@ const displayProperties = computed(() => { height: 1rem; margin-top: -0.3rem; } + .timeout-status { display: inline-block; font-size: 1rem; @@ -406,10 +380,42 @@ const displayProperties = computed(() => { color: #00a3c4; } +/* Styles for DiffViewer integration */ +.saga-state-container { + padding: 0.5rem; +} + +.saga-state-title { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + font-weight: bold; +} + +.json-container { + max-height: 300px; + overflow-y: auto; + background-color: #f8f8f8; +} +.json-view { + margin: 0; + padding: 8px; + white-space: pre-wrap; + font-family: monospace; + font-size: 0.75rem; + line-height: 1.4; +} + +.no-changes-message { + padding: 1rem; + text-align: center; + font-style: italic; + color: #666; +} + @-webkit-keyframes blink-border { 0%, 100% { - border-color: #000000; + border-color: #00a3c4; } 20%, 60% { @@ -417,14 +423,14 @@ const displayProperties = computed(() => { } 40%, 80% { - border-color: #000000; + border-color: #00a3c4; } } @-moz-keyframes blink-border { 0%, 100% { - border-color: #000000; + border-color: #00a3c4; } 20%, 60% { @@ -432,14 +438,14 @@ const displayProperties = computed(() => { } 40%, 80% { - border-color: #000000; + border-color: #00a3c4; } } @-o-keyframes blink-border { 0%, 100% { - border-color: #000000; + border-color: #00a3c4; } 20%, 60% { @@ -447,7 +453,7 @@ const displayProperties = computed(() => { } 40%, 80% { - border-color: #000000; + border-color: #00a3c4; } } From a026e3043dfdd4b8d9290c26949f3d8b189ba7c6 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 1 May 2025 03:24:44 -0600 Subject: [PATCH 05/15] Using codeEditor to display initial state --- src/Frontend/src/components/CodeEditor.vue | 3 ++- .../{DiffViewerV2.vue => DiffViewer.vue} | 2 +- .../messages2/SagaDiagram/SagaUpdateNode.vue | 23 ++++++++----------- 3 files changed, 13 insertions(+), 15 deletions(-) rename src/Frontend/src/components/messages2/{DiffViewerV2.vue => DiffViewer.vue} (99%) diff --git a/src/Frontend/src/components/CodeEditor.vue b/src/Frontend/src/components/CodeEditor.vue index 2d1c0020d..15d825466 100644 --- a/src/Frontend/src/components/CodeEditor.vue +++ b/src/Frontend/src/components/CodeEditor.vue @@ -19,6 +19,7 @@ const props = withDefaults( showGutter?: boolean; showCopyToClipboard?: boolean; ariaLabel?: string; + css?: string; }>(), { readOnly: true, showGutter: true, showCopyToClipboard: true } ); @@ -49,7 +50,7 @@ const extensions = computed(() => {