import { useState, useMemo } from 'react'; import { useParams, useNavigate, Link } from 'react-router'; import { Badge, StatusDot, DataTable, Tabs, AreaChart, LineChart, BarChart, RouteFlow, Spinner, MonoText, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system'; import { useRouteCatalog } from '../../api/queries/catalog'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useProcessorMetrics } from '../../api/queries/processor-metrics'; import { useStatsTimeseries, useSearchExecutions } from '../../api/queries/executions'; import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types'; import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'; import styles from './RouteDetail.module.css'; interface ExchangeRow extends ExecutionSummary { id: string; } interface ProcessorRow { id: string; processorId: string; callCount: number; avgDurationMs: number; p99DurationMs: number; errorCount: number; } interface ErrorPattern { message: string; count: number; lastSeen: string; } export default function RouteDetail() { const { appId, routeId } = useParams(); const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const [activeTab, setActiveTab] = useState('performance'); const { data: catalog } = useRouteCatalog(); const { data: diagram } = useDiagramByRoute(appId, routeId); const { data: processorMetrics, isLoading: processorLoading } = useProcessorMetrics(routeId ?? null, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId); const { data: recentResult, isLoading: recentLoading } = useSearchExecutions({ timeFrom, timeTo, routeId: routeId || undefined, application: appId || undefined, offset: 0, limit: 50, }); const { data: errorResult } = useSearchExecutions({ timeFrom, timeTo, routeId: routeId || undefined, application: appId || undefined, status: 'FAILED', offset: 0, limit: 200, }); const appEntry: AppCatalogEntry | undefined = useMemo(() => (catalog || []).find((e: AppCatalogEntry) => e.appId === appId), [catalog, appId], ); const routeSummary: RouteSummary | undefined = useMemo(() => appEntry?.routes?.find((r: RouteSummary) => r.routeId === routeId), [appEntry, routeId], ); const health = appEntry?.health ?? 'unknown'; const exchangeCount = routeSummary?.exchangeCount ?? 0; const lastSeen = routeSummary?.lastSeen ? new Date(routeSummary.lastSeen).toLocaleString() : '—'; const healthVariant = useMemo((): 'success' | 'warning' | 'error' | 'dead' => { const h = health.toLowerCase(); if (h === 'healthy') return 'success'; if (h === 'degraded') return 'warning'; if (h === 'unhealthy') return 'error'; return 'dead'; }, [health]); const diagramNodes = useMemo(() => { if (!diagram?.nodes) return []; return mapDiagramToRouteNodes(diagram.nodes, []); }, [diagram]); const processorRows: ProcessorRow[] = useMemo(() => (processorMetrics || []).map((p: any) => ({ id: p.processorId, processorId: p.processorId, callCount: p.callCount ?? 0, avgDurationMs: p.avgDurationMs ?? 0, p99DurationMs: p.p99DurationMs ?? 0, errorCount: p.errorCount ?? 0, })), [processorMetrics], ); const chartData = useMemo(() => (timeseries?.buckets || []).map((b: any) => ({ time: new Date(b.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), throughput: b.totalCount, latency: b.avgDurationMs, errors: b.failedCount, successRate: b.totalCount > 0 ? ((b.totalCount - b.failedCount) / b.totalCount) * 100 : 100, })), [timeseries], ); const exchangeRows: ExchangeRow[] = useMemo(() => (recentResult?.data || []).map((e: ExecutionSummary) => ({ ...e, id: e.executionId })), [recentResult], ); const errorPatterns: ErrorPattern[] = useMemo(() => { const failed = (errorResult?.data || []) as ExecutionSummary[]; const grouped = new Map(); for (const ex of failed) { const msg = ex.errorMessage || 'Unknown error'; const existing = grouped.get(msg); if (!existing) { grouped.set(msg, { count: 1, lastSeen: ex.startTime ?? '' }); } else { existing.count += 1; if ((ex.startTime ?? '') > existing.lastSeen) { existing.lastSeen = ex.startTime ?? ''; } } } return Array.from(grouped.entries()) .map(([message, { count, lastSeen: ls }]) => ({ message, count, lastSeen: ls ? new Date(ls).toLocaleString() : '—', })) .sort((a, b) => b.count - a.count); }, [errorResult]); const processorColumns: Column[] = [ { key: 'processorId', header: 'Processor', render: (v) => {String(v)} }, { key: 'callCount', header: 'Calls', sortable: true }, { key: 'avgDurationMs', header: 'Avg', sortable: true, render: (v) => `${(v as number).toFixed(1)}ms` }, { key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(1)}ms` }, { key: 'errorCount', header: 'Errors', sortable: true, render: (v) => { const n = v as number; return n > 0 ? {n} : 0; }}, ]; const exchangeColumns: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (v) => , }, { key: 'executionId', header: 'Exchange ID', render: (v) => {String(v).slice(0, 12)} }, { key: 'startTime', header: 'Started', sortable: true, render: (v) => new Date(v as string).toLocaleTimeString() }, { key: 'durationMs', header: 'Duration', sortable: true, render: (v) => `${v}ms` }, ]; const tabs = [ { label: 'Performance', value: 'performance' }, { label: 'Recent Executions', value: 'executions', count: exchangeRows.length }, { label: 'Error Patterns', value: 'errors', count: errorPatterns.length }, ]; return (
← {appId} routes

{routeId}

Exchanges
{exchangeCount.toLocaleString()}
Last Seen
{lastSeen}
Route Diagram
{diagramNodes.length > 0 ? ( ) : (
No diagram available for this route.
)}
Processor Stats
{processorLoading ? ( ) : processorRows.length > 0 ? ( ) : (
No processor data available.
)}
{activeTab === 'performance' && (
Throughput
({ x: i, y: d.throughput })) }]} height={200} />
Latency
({ x: i, y: d.latency })) }]} height={200} />
Errors
({ x: d.time, y: d.errors })) }]} height={200} />
Success Rate
({ x: i, y: d.successRate })) }]} height={200} />
)} {activeTab === 'executions' && (
{recentLoading ? (
) : ( navigate(`/exchanges/${row.executionId}`)} sortable pageSize={20} /> )}
)} {activeTab === 'errors' && (
{errorPatterns.length === 0 ? (
No error patterns found in the selected time range.
) : ( errorPatterns.map((ep, i) => (
{ep.message} {ep.count}x {ep.lastSeen}
)) )}
)}
); }