import { useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { AlertTriangle } from 'lucide-react' import styles from './RouteDetail.module.css' // Layout import { TopBar } from '../../design-system/layout/TopBar/TopBar' // Composites import { ProcessorTimeline } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline' import { DataTable } from '../../design-system/composites/DataTable/DataTable' import type { Column } from '../../design-system/composites/DataTable/types' // 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 { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout' // Mock data import { routes } from '../../mocks/routes' import { exchanges, type Exchange } from '../../mocks/exchanges' // ─── 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 formatTimestamp(date: Date): string { return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' { switch (status) { case 'completed': return 'success' case 'failed': return 'error' case 'running': return 'running' case 'warning': return 'warning' } } function statusLabel(status: Exchange['status']): string { switch (status) { case 'completed': return 'OK' case 'failed': return 'ERR' case 'running': return 'RUN' case 'warning': return 'WARN' } } function routeStatusVariant(status: 'healthy' | 'degraded' | 'down'): 'success' | 'warning' | 'error' { switch (status) { case 'healthy': return 'success' case 'degraded': return 'warning' case 'down': return 'error' } } function durationClass(ms: number, status: Exchange['status']): string { if (status === 'failed') return styles.durBreach if (ms < 100) return styles.durFast if (ms < 200) return styles.durNormal if (ms < 300) return styles.durSlow return styles.durBreach } // ─── Columns for exchanges table ──────────────────────────────────────────── const EXCHANGE_COLUMNS: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (_, row) => ( {statusLabel(row.status)} ), }, { key: 'id', header: 'Exchange ID', render: (_, row) => {row.id}, }, { key: 'orderId', header: 'Order ID', sortable: true, render: (_, row) => {row.orderId}, }, { key: 'customer', header: 'Customer', render: (_, row) => {row.customer}, }, { key: 'timestamp', header: 'Started', sortable: true, render: (_, row) => {formatTimestamp(row.timestamp)}, }, { key: 'durationMs', header: 'Duration', sortable: true, render: (_, row) => ( {formatDuration(row.durationMs)} ), }, { key: 'agent', header: 'Agent', render: (_, row) => ( {row.agent} ), }, ] // ─── RouteDetail component ──────────────────────────────────────────────────── export function RouteDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const route = useMemo(() => routes.find((r) => r.id === id), [id]) const routeExchanges = useMemo( () => exchanges.filter((e) => e.route === id), [id], ) // Error patterns grouped by exception class const errorPatterns = useMemo(() => { const patterns: Record = {} for (const exec of routeExchanges) { if (exec.status === 'failed' && exec.errorClass) { if (!patterns[exec.errorClass]) { patterns[exec.errorClass] = { count: 0, lastMessage: exec.errorMessage ?? '', lastTime: exec.timestamp, } } patterns[exec.errorClass].count++ if (exec.timestamp > patterns[exec.errorClass].lastTime) { patterns[exec.errorClass].lastTime = exec.timestamp patterns[exec.errorClass].lastMessage = exec.errorMessage ?? '' } } } return Object.entries(patterns) }, [routeExchanges]) // Build aggregate processor timeline from all exchanges for this route const aggregateProcessors = useMemo(() => { if (routeExchanges.length === 0) return [] // Use the first exchange's processors as the template, with averaged durations const templateExec = routeExchanges[0] if (!templateExec) return [] return templateExec.processors.map((proc) => { const allDurations = routeExchanges .flatMap((e) => e.processors) .filter((p) => p.name === proc.name) .map((p) => p.durationMs) const avgDuration = allDurations.length ? Math.round(allDurations.reduce((a, b) => a + b, 0) / allDurations.length) : proc.durationMs const hasFailures = routeExchanges.some((e) => e.processors.some((p) => p.name === proc.name && p.status === 'fail'), ) const hasSlows = routeExchanges.some((e) => e.processors.some((p) => p.name === proc.name && p.status === 'slow'), ) return { ...proc, durationMs: avgDuration, status: hasFailures ? ('fail' as const) : hasSlows ? ('slow' as const) : ('ok' as const), } }) }, [routeExchanges]) const totalAggregateMs = aggregateProcessors.reduce((sum, p) => sum + p.durationMs, 0) const inflightCount = routeExchanges.filter((e) => e.status === 'running').length const successCount = routeExchanges.filter((e) => e.status === 'completed').length const errorCount = routeExchanges.filter((e) => e.status === 'failed').length const successRate = routeExchanges.length ? ((successCount / routeExchanges.length) * 100).toFixed(1) : '0.0' // Not found state if (!route) { return ( <>
Route "{id}" not found in mock data.
) } const statusVariant = routeStatusVariant(route.status) return ( <> {/* Top bar */} {/* Scrollable content */}
{/* Route header card */}

{route.name}

{route.group}

{route.description}

{/* KPI strip */}
Total Exchanges
{route.exchangeCount.toLocaleString()}
Success Rate
= 99 ? styles.kpiGood : route.successRate >= 97 ? styles.kpiWarn : styles.kpiError }`}> {route.successRate}%
Avg Latency (p50)
{route.avgDurationMs}ms
p99 Latency
300 ? styles.kpiError : route.p99DurationMs > 200 ? styles.kpiWarn : styles.kpiGood}`}> {route.p99DurationMs}ms
Inflight
0 ? styles.kpiRunning : ''}`}> {inflightCount}
{/* Processor timeline (aggregate view) */}
Processor Performance (aggregate avg) Based on {routeExchanges.length} exchanges
{aggregateProcessors.length > 0 ? ( ) : (
No exchange data for this route in mock set.
)}
{/* Recent exchanges table */}
Recent Exchanges
{routeExchanges.length} exchanges · {errorCount} errors = 99 ? 'success' : parseFloat(successRate) >= 97 ? 'warning' : 'error'} />
{ if (row.status === 'failed') return 'error' if (row.status === 'warning') return 'warning' return undefined }} expandedContent={(row) => row.errorMessage ? (
{row.errorClass}
{row.errorMessage}
) : null } onRowClick={(row) => navigate(`/exchanges/${row.id}`)} />
{/* Error patterns section */} {errorPatterns.length > 0 && (
Error Patterns {errorPatterns.length} distinct exception types
{errorPatterns.map(([cls, info]) => (
{cls}
{info.lastMessage}
Last seen: {formatTimestamp(info.lastTime)}
))}
)}
) }