import { useMemo } from 'react' import { useNavigate, useParams } from 'react-router-dom' import styles from './Routes.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 { AreaChart } from '../../design-system/composites/AreaChart/AreaChart' import { LineChart } from '../../design-system/composites/LineChart/LineChart' import { BarChart } from '../../design-system/composites/BarChart/BarChart' import { DataTable } from '../../design-system/composites/DataTable/DataTable' import type { Column } from '../../design-system/composites/DataTable/types' import { RouteFlow } from '../../design-system/composites/RouteFlow/RouteFlow' import type { RouteNode } from '../../design-system/composites/RouteFlow/RouteFlow' // Primitives import { Sparkline } from '../../design-system/primitives/Sparkline/Sparkline' import { MonoText } from '../../design-system/primitives/MonoText/MonoText' import { Badge } from '../../design-system/primitives/Badge/Badge' // Mock data import { throughputSeries, latencySeries, errorCountSeries, routeMetrics, type RouteMetricRow, } from '../../mocks/metrics' import { routes } from '../../mocks/routes' import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar' const ROUTE_TO_APP = buildRouteToAppMap() // ─── KPI Header Strip (matches mock-v3-metrics-dashboard) ──────────────────── function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) { const totalExchanges = scopedMetrics.reduce((sum, r) => sum + r.exchangeCount, 0) const totalErrors = scopedMetrics.reduce((sum, r) => sum + r.errorCount, 0) const errorRate = totalExchanges > 0 ? ((totalErrors / totalExchanges) * 100) : 0 const avgLatency = scopedMetrics.length > 0 ? Math.round(scopedMetrics.reduce((sum, r) => sum + r.avgDurationMs, 0) / scopedMetrics.length) : 0 const p99Latency = scopedMetrics.length > 0 ? Math.max(...scopedMetrics.map((r) => r.p99DurationMs)) : 0 const avgSuccessRate = scopedMetrics.length > 0 ? Number((scopedMetrics.reduce((sum, r) => sum + r.successRate, 0) / scopedMetrics.length).toFixed(1)) : 0 const throughputPerSec = totalExchanges > 0 ? (totalExchanges / 360).toFixed(1) : '0' const activeRoutes = scopedMetrics.length const totalRoutes = routeMetrics.length return (
{/* Card 1: Total Throughput */}
Total Throughput
{totalExchanges.toLocaleString()} exchanges ▲ +8%
{throughputPerSec} msg/s · Capacity 39%
{/* Card 2: System Error Rate */}
System Error Rate
{errorRate.toFixed(2)}% {errorRate < 1 ? '\u25BC -0.1%' : '\u25B2 +0.4%'}
{totalErrors} errors / {totalExchanges.toLocaleString()} total (6h)
{/* Card 3: Latency Percentiles */}
300 ? styles.kpiCardWarn : styles.kpiCardGreen}`}>
Latency Percentiles
P50 {Math.round(avgLatency * 0.5)}ms ▼3
P95 150 ? styles.latValAmber : styles.latValGreen}`}>{Math.round(avgLatency * 1.4)}ms ▲12
P99 300 ? styles.latValRed : styles.latValAmber}`}>{p99Latency}ms ▲28
SLA: <300ms P99 · {p99Latency > 300 ? BREACH : OK}
{/* Card 4: Active Routes */}
Active Routes
{activeRoutes} of {totalRoutes} ↔ stable
{activeRoutes} active {totalRoutes - activeRoutes} stopped
{/* Card 5: In-Flight Exchanges */}
In-Flight Exchanges
23
High-water: 67 (2h ago)
) } // ─── Route metric row with id field (required by DataTable) ────────────────── type RouteMetricRowWithId = RouteMetricRow & { id: string } // ─── Processor metrics types and generator ─────────────────────────────────── interface ProcessorMetric { name: string type: string invocations: number avgDurationMs: number p99DurationMs: number errorCount: number errorRate: number sparkline: number[] } type ProcessorMetricWithId = ProcessorMetric & { id: string } function generateProcessorMetrics(processors: string[], routeExchangeCount: number): ProcessorMetric[] { return processors.map((proc, i) => { const name = proc const type = proc.startsWith('from(') ? 'consumer' : proc.startsWith('to(') ? 'producer' : proc.startsWith('enrich(') ? 'enricher' : proc.startsWith('validate(') || proc.startsWith('check(') ? 'validator' : proc.startsWith('unmarshal(') || proc.startsWith('marshal(') ? 'transformer' : proc.startsWith('route(') || proc.startsWith('choice(') ? 'router' : 'processor' const invocations = routeExchangeCount const avgBase = 10 + (i * 15) + (proc.includes('enrich') ? 40 : 0) + (proc.includes('http') ? 80 : 0) const avgDurationMs = avgBase + Math.round(Math.sin(i * 2.1) * 10) const p99DurationMs = Math.round(avgDurationMs * 2.5) const errorCount = proc.includes('enrich') || proc.includes('http') ? Math.round(invocations * 0.01) : Math.round(invocations * 0.001) const errorRate = Number(((errorCount / invocations) * 100).toFixed(2)) const sparkline = Array.from({ length: 14 }, (_, j) => avgDurationMs + Math.round(Math.sin(j * 0.8 + i) * avgDurationMs * 0.15)) return { name, type, invocations, avgDurationMs, p99DurationMs, errorCount, errorRate, sparkline } }) } // ─── Map processor type to RouteNode type ──────────────────────────────────── function toRouteNodeType(procType: string): RouteNode['type'] { switch (procType) { case 'consumer': return 'from' case 'producer': return 'to' case 'enricher': return 'process' case 'validator': return 'process' case 'transformer': return 'process' case 'router': return 'choice' default: return 'process' } } // ─── Processor type badge classes ──────────────────────────────────────────── const TYPE_STYLE_MAP: Record = { consumer: styles.typeConsumer, producer: styles.typeProducer, enricher: styles.typeEnricher, validator: styles.typeValidator, transformer: styles.typeTransformer, router: styles.typeRouter, processor: styles.typeProcessor, } // ─── Route performance table columns ────────────────────────────────────────── const ROUTE_COLUMNS: Column[] = [ { key: 'routeName', header: 'Route', sortable: true, render: (_, row) => ( {row.routeName} ), }, { key: 'appId', header: 'Application', sortable: true, render: (_, row) => ( {row.appId} ), }, { key: 'exchangeCount', header: 'Exchanges', sortable: true, render: (_, row) => ( {row.exchangeCount.toLocaleString()} ), }, { key: 'successRate', header: 'Success %', sortable: true, render: (_, row) => { const cls = row.successRate >= 99 ? styles.rateGood : row.successRate >= 97 ? styles.rateWarn : styles.rateBad return {row.successRate.toFixed(1)}% }, }, { key: 'avgDurationMs', header: 'Avg Duration', sortable: true, render: (_, row) => ( {row.avgDurationMs}ms ), }, { key: 'p99DurationMs', header: 'p99 Duration', sortable: true, render: (_, row) => { const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood return {row.p99DurationMs}ms }, }, { key: 'errorCount', header: 'Errors', sortable: true, render: (_, row) => ( 10 ? styles.rateBad : styles.rateNeutral}> {row.errorCount} ), }, { key: 'sparkline', header: 'Trend', render: (_, row) => ( ), }, ] // ─── Processor performance table columns ───────────────────────────────────── const PROCESSOR_COLUMNS: Column[] = [ { key: 'name', header: 'Processor', sortable: true, render: (_, row) => ( {row.name} ), }, { key: 'type', header: 'Type', sortable: true, render: (_, row) => ( {row.type} ), }, { key: 'invocations', header: 'Invocations', sortable: true, render: (_, row) => ( {row.invocations.toLocaleString()} ), }, { key: 'avgDurationMs', header: 'Avg Duration', sortable: true, render: (_, row) => { const cls = row.avgDurationMs > 200 ? styles.rateBad : row.avgDurationMs > 100 ? styles.rateWarn : styles.rateGood return {row.avgDurationMs}ms }, }, { key: 'p99DurationMs', header: 'p99 Duration', sortable: true, render: (_, row) => { const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood return {row.p99DurationMs}ms }, }, { key: 'errorCount', header: 'Errors', sortable: true, render: (_, row) => ( 10 ? styles.rateBad : styles.rateNeutral}> {row.errorCount} ), }, { key: 'errorRate', header: 'Error Rate', sortable: true, render: (_, row) => { const cls = row.errorRate > 1 ? styles.rateBad : row.errorRate > 0.5 ? styles.rateWarn : styles.rateGood return {row.errorRate}% }, }, { key: 'sparkline', header: 'Trend', render: (_, row) => ( ), }, ] // ─── Build bar chart data from error series ──────────────────────────────────── function buildErrorBarSeries() { const sampleInterval = 5 return errorCountSeries.map((s) => ({ label: s.label, data: s.data .filter((_, i) => i % sampleInterval === 0) .map((pt) => ({ x: pt.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), y: Math.round(pt.value), })), })) } // ─── Build volume area chart (derived from throughput) ───────────────────────── function buildVolumeSeries() { return throughputSeries.map((s) => ({ label: s.label, data: s.data.map((pt) => ({ x: pt.timestamp, y: Math.round(pt.value * 60), })), })) } const ERROR_BAR_SERIES = buildErrorBarSeries() const VOLUME_SERIES = buildVolumeSeries() // Convert MetricSeries (from mocks) to ChartSeries format function convertSeries(series: typeof throughputSeries) { return series.map((s) => ({ label: s.label, data: s.data.map((pt) => ({ x: pt.timestamp, y: pt.value })), })) } // ─── Routes page ────────────────────────────────────────────────────────────── export function Routes() { const navigate = useNavigate() const { appId, routeId } = useParams<{ appId: string; routeId: string }>() // ── Breadcrumbs ───────────────────────────────────────────────────────────── const breadcrumb = useMemo(() => { if (routeId && appId) { return [ { label: 'Routes', href: '/routes' }, { label: appId, href: `/routes/${appId}` }, { label: routeId }, ] } if (appId) { return [ { label: 'Routes', href: '/routes' }, { label: appId }, ] } return [{ label: 'Routes' }] }, [appId, routeId]) // ── Data filtering ────────────────────────────────────────────────────────── const filteredMetrics = useMemo(() => { const data = appId ? routeMetrics.filter((r) => r.appId === appId) : routeMetrics return data.map((r) => ({ ...r, id: r.routeId })) }, [appId]) // ── Route detail data ─────────────────────────────────────────────────────── const routeDef = useMemo(() => { if (!routeId) return null return routes.find((r) => r.id === routeId) ?? null }, [routeId]) const processorMetrics = useMemo(() => { if (!routeDef) return [] return generateProcessorMetrics(routeDef.processors, routeDef.exchangeCount).map((pm, i) => ({ ...pm, id: `proc-${i}`, })) }, [routeDef]) const routeFlowNodes = useMemo(() => { if (!processorMetrics.length) return [] return processorMetrics.map((pm) => ({ name: pm.name, type: toRouteNodeType(pm.type), durationMs: pm.avgDurationMs, status: pm.errorRate > 1 ? 'fail' as const : pm.avgDurationMs > 150 ? 'slow' as const : 'ok' as const, })) }, [processorMetrics]) // Scoped metrics for KPI header const scopedMetricsForKpi = useMemo(() => { if (routeId) return routeMetrics.filter((r) => r.routeId === routeId) if (appId) return routeMetrics.filter((r) => r.appId === appId) return routeMetrics }, [appId, routeId]) // ── Route detail view ─────────────────────────────────────────────────────── if (routeId && appId && routeDef) { return ( }>
Auto-refresh: 30s
{/* Processor Performance table */}
Processor Performance
{processorMetrics.length} processors
{/* Route Flow diagram */}
Route Flow
) } // ── Top level / Application level view ────────────────────────────────────── return ( }>
Auto-refresh: 30s
{/* KPI header cards */} {/* Per-route performance table */}
Per-Route Performance
{filteredMetrics.length} routes
{ const rowAppId = appId ?? ROUTE_TO_APP.get(row.routeId) ?? row.routeId navigate(`/routes/${rowAppId}/${row.routeId}`) }} />
{/* 2x2 chart grid */}
Throughput (msg/s)
Latency (ms)
Errors by Route
Message Volume (msg/min)
) }