From 021a52e56b2d63d970c47e4166c71ac0137d2ce9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:12:11 +0100 Subject: [PATCH] feat: integrate ExecutionDiagram into ExchangeDetail flow view Replace the RouteFlow-based flow view with the new ExecutionDiagram component which provides execution overlay, iteration stepping, and an integrated detail panel. The gantt view and all other page sections remain unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ExchangeDetail/ExchangeDetail.module.css | 11 ++ .../pages/ExchangeDetail/ExchangeDetail.tsx | 120 ++++++++---------- 2 files changed, 65 insertions(+), 66 deletions(-) diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css b/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css index 5c977839..2d18e19f 100644 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css +++ b/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css @@ -265,6 +265,17 @@ padding: 12px 16px; } +/* ========================================================================== + EXECUTION DIAGRAM CONTAINER (Flow view) + ========================================================================== */ +.executionDiagramContainer { + height: 600px; + border: 1px solid var(--border, #E4DFD8); + border-radius: var(--radius-md, 8px); + overflow: hidden; + margin-bottom: 16px; +} + /* ========================================================================== DETAIL SPLIT (IN / OUT panels) ========================================================================== */ diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx index afdb6ce5..70216452 100644 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx +++ b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx @@ -2,19 +2,21 @@ import { useState, useMemo, useCallback, useEffect } from 'react' import { useParams, useNavigate } from 'react-router' import { Badge, StatusDot, MonoText, CodeBlock, InfoCallout, - ProcessorTimeline, Spinner, RouteFlow, useToast, + ProcessorTimeline, Spinner, useToast, LogViewer, ButtonGroup, SectionHeader, useBreadcrumb, Modal, Tabs, Button, Select, Input, Textarea, + useGlobalFilters, } from '@cameleer/design-system' -import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system' +import type { ProcessorStep, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system' import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions' import { useCorrelationChain } from '../../api/queries/correlation' -import { useDiagramLayout } from '../../api/queries/diagrams' -import { buildFlowSegments, toFlowSegments } from '../../utils/diagram-mapping' import { useTracingStore } from '../../stores/tracing-store' import { useApplicationConfig, useUpdateApplicationConfig, useReplayExchange } from '../../api/queries/commands' import { useAgents } from '../../api/queries/agents' import { useApplicationLogs } from '../../api/queries/logs' +import { useRouteCatalog } from '../../api/queries/catalog' +import { ExecutionDiagram } from '../../components/ExecutionDiagram' +import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types' import styles from './ExchangeDetail.module.css' const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ @@ -88,7 +90,6 @@ export default function ExchangeDetail() { const { data: detail, isLoading } = useExecutionDetail(id ?? null) const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null) - const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null) const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt') const [logSearch, setLogSearch] = useState('') @@ -170,33 +171,6 @@ export default function ExchangeDetail() { const inputBody = snapshot?.inputBody ?? null const outputBody = snapshot?.outputBody ?? null - // Build RouteFlow nodes from diagram + execution data, split into flow segments - const { routeFlows, flowNodeIds } = useMemo(() => { - if (diagram?.nodes) { - const { flows, nodeIds } = buildFlowSegments(diagram.nodes, procList) - // Apply badges to each node across all flows - let idx = 0 - const badgedFlows = flows.map(flow => ({ - ...flow, - nodes: flow.nodes.map(node => ({ - ...node, - badges: badgesFor(nodeIds[idx++]), - })), - })) - return { routeFlows: badgedFlows, flowNodeIds: nodeIds } - } - // Fallback: build from processor list (no diagram available) - const nodes = processors.map((p) => ({ - name: p.name, - type: 'process' as RouteNode['type'], - durationMs: p.durationMs, - status: p.status, - badges: badgesFor(p.name), - })) - const { flows } = toFlowSegments(nodes) - return { routeFlows: flows, flowNodeIds: [] as string[] } - }, [diagram, processors, procList, tracedMap]) - // ProcessorId lookup: timeline index → processorId const processorIds: string[] = useMemo(() => { const ids: string[] = [] @@ -208,12 +182,6 @@ export default function ExchangeDetail() { return ids }, [procList]) - // Map flow display index → processor tree index (for snapshot API) - // flowNodeIds already contains processor IDs in flow-order - const flowToTreeIndex = useMemo(() => - flowNodeIds.map(pid => pid ? processorIds.indexOf(pid) : -1), - [flowNodeIds, processorIds], - ) // ── Tracing toggle ────────────────────────────────────────────────────── const { toast } = useToast() @@ -248,6 +216,36 @@ export default function ExchangeDetail() { }) }, [detail, app, appConfig, tracingStore, updateConfig, toast]) + // ── ExecutionDiagram support ────────────────────────────────────────── + const { timeRange } = useGlobalFilters() + const { data: catalog } = useRouteCatalog( + timeRange.start.toISOString(), + timeRange.end.toISOString(), + ) + + const knownRouteIds = useMemo(() => { + if (!catalog || !app) return new Set() + const appEntry = (catalog as Array<{ appId: string; routes?: Array<{ routeId: string }> }>) + .find(a => a.appId === app) + return new Set((appEntry?.routes ?? []).map(r => r.routeId)) + }, [catalog, app]) + + const nodeConfigs = useMemo(() => { + const map = new Map() + if (tracedMap) { + for (const pid of Object.keys(tracedMap)) { + map.set(pid, { traceEnabled: true }) + } + } + return map + }, [tracedMap]) + + const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => { + if (action === 'toggle-trace') { + handleToggleTracing(nodeId) + } + }, [handleToggleTracing]) + // ── Replay ───────────────────────────────────────────────────────────── const { data: liveAgents } = useAgents('LIVE', detail?.applicationName) const replay = useReplayExchange() @@ -461,9 +459,9 @@ export default function ExchangeDetail() { -
- {timelineView === 'gantt' ? ( - processors.length > 0 ? ( + {timelineView === 'gantt' && ( +
+ {processors.length > 0 ? ( ) : ( No processor data available - ) - ) : ( - routeFlows.length > 0 ? ( - { - const treeIdx = flowToTreeIndex[index] - if (treeIdx >= 0) setSelectedProcessorIndex(treeIdx) - }} - selectedIndex={flowToTreeIndex.indexOf(activeIndex)} - getActions={(_node, index) => { - const pid = flowNodeIds[index] ?? '' - if (!pid || !detail?.applicationName) return [] - return [{ - label: tracingStore.isTraced(app, pid) ? 'Disable Tracing' : 'Enable Tracing', - onClick: () => handleToggleTracing(pid), - disabled: updateConfig.isPending, - }] - }} - /> - ) : ( - - ) - )} -
+ )} +
+ )} + {timelineView === 'flow' && detail && ( +
+ +
+ )} + {/* Exchange-level body (start/end of route) */} {detail && (detail.inputBody || detail.outputBody) && (