import React, { useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { Badge, StatusDot, MonoText, CodeBlock, InfoCallout, ProcessorTimeline, Breadcrumb, Spinner, RouteFlow, } from '@cameleer/design-system'; import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'; import { useCorrelationChain } from '../../api/queries/correlation'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'; import styles from './ExchangeDetail.module.css'; function countProcessors(nodes: any[]): number { return nodes.reduce((sum, n) => sum + 1 + countProcessors(n.children || []), 0); } function formatDuration(ms: number): string { if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`; if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; return `${ms}ms`; } function parseHeaders(raw: string | undefined | null): Record { if (!raw) return {}; try { const parsed = JSON.parse(raw); if (typeof parsed === 'object' && parsed !== null) { const result: Record = {}; for (const [k, v] of Object.entries(parsed)) { result[k] = typeof v === 'string' ? v : JSON.stringify(v); } return result; } } catch { /* ignore */ } return {}; } export default function ExchangeDetail() { const { id } = useParams(); const navigate = useNavigate(); const { data: detail, isLoading } = useExecutionDetail(id ?? null); const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt'); const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null); const { data: diagram } = useDiagramByRoute(detail?.groupName, detail?.routeId); const procList = detail ? (detail.processors?.length ? detail.processors : (detail.children ?? [])) : []; // Auto-select first failed processor, or 0 const defaultIndex = useMemo(() => { if (!procList.length) return 0; const failIdx = procList.findIndex((p: any) => (p.status || '').toUpperCase() === 'FAILED' || p.status === 'fail' ); return failIdx >= 0 ? failIdx : 0; }, [procList]); const [selectedProcessorIndex, setSelectedProcessorIndex] = useState(null); const activeIndex = selectedProcessorIndex ?? defaultIndex; const { data: snapshot } = useProcessorSnapshot(id ?? null, procList.length > 0 ? activeIndex : null); const processors = useMemo(() => { if (!procList.length) return []; const result: any[] = []; let offset = 0; function walk(node: any) { result.push({ name: node.processorId || node.processorType, type: node.processorType, durationMs: node.durationMs ?? 0, status: node.status === 'COMPLETED' ? 'ok' : node.status === 'FAILED' ? 'fail' : 'ok', startMs: offset, }); offset += node.durationMs ?? 0; if (node.children) node.children.forEach(walk); } procList.forEach(walk); return result; }, [procList]); const selectedProc = processors[activeIndex]; const isSelectedFailed = selectedProc?.status === 'fail'; // Parse snapshot headers const inputHeaders = parseHeaders(snapshot?.inputHeaders); const outputHeaders = parseHeaders(snapshot?.outputHeaders); const inputBody = snapshot?.inputBody ?? null; const outputBody = snapshot?.outputBody ?? null; if (isLoading) return
; if (!detail) return Exchange not found; return (
{/* Exchange header card */}
{id}
Route: navigate(`/apps/${detail.groupName}/${detail.routeId}`)}>{detail.routeId} {detail.groupName && ( <> · App: {detail.groupName} )}
Duration
{formatDuration(detail.durationMs)}
Agent
{detail.agentId}
Started
{detail.startTime ? new Date(detail.startTime).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—'}
Processors
{countProcessors(procList)}
{/* Correlation Chain */} {correlationData?.data && correlationData.data.length > 1 && (
Correlated Exchanges {correlationData.data.map((exec: any) => { const isCurrent = exec.executionId === id; const variant = exec.status === 'COMPLETED' ? 'success' : exec.status === 'FAILED' ? 'error' : 'running'; const statusCls = variant === 'success' ? styles.chainNodeSuccess : variant === 'error' ? styles.chainNodeError : styles.chainNodeRunning; return ( ); })} {correlationData.total > 20 && ( +{correlationData.total - 20} more )}
)}
{/* Error callout */} {detail.errorMessage && ( {detail.errorMessage} )} {/* Processor Timeline / Flow Section */}
Processor Timeline {processors.length} processors
{timelineView === 'gantt' ? ( processors.length > 0 ? ( setSelectedProcessorIndex(i)} selectedIndex={activeIndex} /> ) : ( No processor data available ) ) : ( diagram ? ( setSelectedProcessorIndex(i)} selectedIndex={activeIndex} /> ) : ( ) )}
{/* Processor Detail: Message IN / Message OUT or Error */} {selectedProc && snapshot && (
{/* Message IN */}
Message IN at processor #{activeIndex + 1} entry
{Object.keys(inputHeaders).length > 0 && (
Headers {Object.keys(inputHeaders).length}
{Object.entries(inputHeaders).map(([key, value]) => (
{key} {value}
))}
)}
Body
{/* Message OUT or Error */} {isSelectedFailed ? (
× Error at Processor #{activeIndex + 1}
{detail.errorMessage && (
{detail.errorMessage}
)}
Processor {selectedProc.name} Duration {formatDuration(selectedProc.durationMs)} Status {selectedProc.status.toUpperCase()}
) : (
Message OUT after processor #{activeIndex + 1}
{Object.keys(outputHeaders).length > 0 && (
Headers {Object.keys(outputHeaders).length}
{Object.entries(outputHeaders).map(([key, value]) => (
{key} {value}
))}
)}
Body
)}
)} {/* No snapshot loaded yet - show prompt */} {selectedProc && !snapshot && procList.length > 0 && (
Loading exchange snapshot...
)}
); }