import { useMemo, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import styles from './ExchangeDetail.module.css' // Layout import { AppShell } from '../../design-system/layout/AppShell/AppShell' import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar' import { TopBar } from '../../design-system/layout/TopBar/TopBar' // Composites import { ProcessorTimeline } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline' import type { ProcessorStep } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline' // Primitives import { Badge } from '../../design-system/primitives/Badge/Badge' import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot' import { MonoText } from '../../design-system/primitives/MonoText/MonoText' import { Collapsible } from '../../design-system/primitives/Collapsible/Collapsible' import { CodeBlock } from '../../design-system/primitives/CodeBlock/CodeBlock' import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout' // Mock data import { executions } from '../../mocks/executions' import { routes } from '../../mocks/routes' import { agents } from '../../mocks/agents' // ─── Sidebar data (shared) ──────────────────────────────────────────────────── const APPS = [ { id: 'order-service', name: 'order-service', agentCount: 2, health: 'live' as const, execCount: 1433 }, { id: 'payment-svc', name: 'payment-svc', agentCount: 1, health: 'live' as const, execCount: 912 }, { id: 'shipment-tracker', name: 'shipment-tracker', agentCount: 2, health: 'live' as const, execCount: 471 }, { id: 'notification-hub', name: 'notification-hub', agentCount: 1, health: 'stale' as const, execCount: 128 }, ] const SIDEBAR_ROUTES = routes.slice(0, 3).map((r) => ({ id: r.id, name: r.name, execCount: r.execCount, })) // ─── Helpers ────────────────────────────────────────────────────────────────── 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 statusToVariant(status: 'completed' | 'failed' | 'running' | 'warning'): 'success' | 'error' | 'running' | 'warning' { switch (status) { case 'completed': return 'success' case 'failed': return 'error' case 'running': return 'running' case 'warning': return 'warning' } } function statusToLabel(status: 'completed' | 'failed' | 'running' | 'warning'): string { switch (status) { case 'completed': return 'COMPLETED' case 'failed': return 'FAILED' case 'running': return 'RUNNING' case 'warning': return 'WARNING' } } // ─── Exchange body mock generator ──────────────────────────────────────────── // For each processor step, generate a plausible exchange body snapshot function generateExchangeSnapshot( step: ProcessorStep, orderId: string, customer: string, stepIndex: number, ) { const baseBody = { orderId, customer, status: step.status === 'fail' ? 'ERROR' : 'PROCESSING', processorStep: step.name, stepIndex, } const headers: Record = { 'CamelCorrelationId': `cmr-${Math.random().toString(36).slice(2, 10)}`, 'Content-Type': 'application/json', 'CamelTimerName': step.name, 'CamelBreadcrumbId': `${orderId}-${stepIndex}`, } if (stepIndex === 0) { return { headers, body: JSON.stringify({ ...baseBody, raw: { orderId, customer, items: ['ITEM-001', 'ITEM-002'], total: 142.50 }, }, null, 2), } } if (step.type === 'enrich') { return { headers: { ...headers, 'enrichedBy': step.name.replace('enrich(', '').replace(')', ''), }, body: JSON.stringify({ ...baseBody, enrichment: { source: step.name, addedFields: ['customerId', 'address', 'tier'] }, }, null, 2), } } return { headers, body: JSON.stringify(baseBody, null, 2), } } // ─── ExchangeDetail component ───────────────────────────────────────────────── export function ExchangeDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const [activeItem, setActiveItem] = useState('') const execution = useMemo(() => executions.find((e) => e.id === id), [id]) function handleItemClick(itemId: string) { setActiveItem(itemId) const route = routes.find((r) => r.id === itemId) if (route) navigate(`/routes/${itemId}`) } // Not found state if (!execution) { return ( } >
Exchange "{id}" not found in mock data.
) } const statusVariant = statusToVariant(execution.status) const statusLabel = statusToLabel(execution.status) return ( } > {/* Top bar */} {/* Scrollable content */}
{/* Exchange header */}
{execution.id}
Route: navigate(`/routes/${execution.route}`)}>{execution.route} · Order: {execution.orderId} · Customer: {execution.customer}
Duration
{formatDuration(execution.durationMs)}
Agent
{execution.agent}
Started
{execution.timestamp.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
Processors
{execution.processors.length}
{/* Processor timeline */}
Processor Timeline Total: {formatDuration(execution.durationMs)}
{/* Step-by-step inspector */}
Exchange Inspector {execution.processors.length} processor steps
{execution.processors.map((proc, index) => { const snapshot = generateExchangeSnapshot(proc, execution.orderId, execution.customer, index) const stepStatusClass = proc.status === 'fail' ? styles.stepFail : proc.status === 'slow' ? styles.stepSlow : styles.stepOk return ( {index + 1} {proc.name} {formatDuration(proc.durationMs)}
} defaultOpen={proc.status === 'fail'} className={styles.stepCollapsible} >
Exchange Headers
Exchange Body
) })}
{/* Error block (if failed) */} {execution.status === 'failed' && execution.errorMessage && (
Error Details
{execution.errorClass}
{execution.errorMessage}
Failed at processor: {execution.processors.find((p) => p.status === 'fail')?.name ?? 'unknown'}
)}
) }