import { useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { KpiStrip, DataTable, AreaChart, LineChart, BarChart, Card, Sparkline, MonoText, Badge, } from '@cameleer/design-system'; import type { KpiItem, Column } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system'; import { useRouteMetrics } from '../../api/queries/catalog'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import type { RouteMetrics } from '../../api/types'; import styles from './RoutesMetrics.module.css'; interface RouteRow { id: string; routeId: string; appId: string; exchangeCount: number; successRate: number; avgDurationMs: number; p99DurationMs: number; errorRate: number; throughputPerSec: number; sparkline: number[]; } // ── Route table columns ────────────────────────────────────────────────────── const ROUTE_COLUMNS: Column[] = [ { key: 'routeId', header: 'Route', sortable: true, render: (_, row) => ( {row.routeId} ), }, { 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 pct = row.successRate * 100; const cls = pct >= 99 ? styles.rateGood : pct >= 97 ? styles.rateWarn : styles.rateBad; return {pct.toFixed(1)}%; }, }, { key: 'avgDurationMs', header: 'Avg Duration', sortable: true, render: (_, row) => ( {Math.round(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 {Math.round(row.p99DurationMs)}ms; }, }, { key: 'errorRate', header: 'Error Rate', sortable: true, render: (_, row) => { const pct = row.errorRate * 100; const cls = pct > 5 ? styles.rateBad : pct > 1 ? styles.rateWarn : styles.rateGood; return {pct.toFixed(1)}%; }, }, { key: 'sparkline', header: 'Trend', render: (_, row) => ( ), }, ]; // ── Build KPI items from backend stats ─────────────────────────────────────── function buildKpiItems( stats: { totalCount: number; failedCount: number; avgDurationMs: number; p99LatencyMs: number; activeCount: number; prevTotalCount: number; prevFailedCount: number; prevP99LatencyMs: number; } | undefined, routeCount: number, throughputSparkline: number[], errorSparkline: number[], ): KpiItem[] { const totalCount = stats?.totalCount ?? 0; const failedCount = stats?.failedCount ?? 0; const prevTotalCount = stats?.prevTotalCount ?? 0; const p99Ms = stats?.p99LatencyMs ?? 0; const prevP99Ms = stats?.prevP99LatencyMs ?? 0; const avgMs = stats?.avgDurationMs ?? 0; const activeCount = stats?.activeCount ?? 0; const errorRate = totalCount > 0 ? (failedCount / totalCount) * 100 : 0; const throughputPctChange = prevTotalCount > 0 ? Math.round(((totalCount - prevTotalCount) / prevTotalCount) * 100) : 0; const throughputTrendLabel = throughputPctChange >= 0 ? `\u25B2 +${throughputPctChange}%` : `\u25BC ${throughputPctChange}%`; const p50 = Math.round(avgMs * 0.5); const p95 = Math.round(avgMs * 1.4); const slaStatus = p99Ms > 300 ? 'BREACH' : 'OK'; const prevErrorRate = prevTotalCount > 0 ? ((stats?.prevFailedCount ?? 0) / prevTotalCount) * 100 : 0; const errorDelta = (errorRate - prevErrorRate).toFixed(1); return [ { label: 'Total Throughput', value: totalCount.toLocaleString(), trend: { label: throughputTrendLabel, variant: throughputPctChange >= 0 ? 'success' as const : 'error' as const, }, subtitle: `${activeCount} active exchanges`, sparkline: throughputSparkline, borderColor: 'var(--amber)', }, { label: 'System Error Rate', value: `${errorRate.toFixed(2)}%`, trend: { label: errorRate <= prevErrorRate ? `\u25BC ${errorDelta}%` : `\u25B2 +${errorDelta}%`, variant: errorRate < 1 ? 'success' as const : 'error' as const, }, subtitle: `${failedCount} errors / ${totalCount.toLocaleString()} total`, sparkline: errorSparkline, borderColor: errorRate < 1 ? 'var(--success)' : 'var(--error)', }, { label: 'Latency Percentiles', value: `${p99Ms}ms`, trend: { label: p99Ms > prevP99Ms ? `\u25B2 +${p99Ms - prevP99Ms}ms` : `\u25BC ${prevP99Ms - p99Ms}ms`, variant: p99Ms > 300 ? 'error' as const : 'warning' as const, }, subtitle: `P50 ${p50}ms \u00B7 P95 ${p95}ms \u00B7 SLA <300ms P99: ${slaStatus}`, borderColor: p99Ms > 300 ? 'var(--warning)' : 'var(--success)', }, { label: 'Active Routes', value: `${routeCount}`, trend: { label: '\u2194 stable', variant: 'muted' as const }, subtitle: `${routeCount} routes reporting`, borderColor: 'var(--running)', }, { label: 'In-Flight Exchanges', value: String(activeCount), trend: { label: '\u2194', variant: 'muted' as const }, subtitle: `${activeCount} active`, sparkline: throughputSparkline, borderColor: 'var(--amber)', }, ]; } // ── Component ──────────────────────────────────────────────────────────────── export default function RoutesMetrics() { const { appId } = useParams(); const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId); const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); // Map backend RouteMetrics[] to table rows const rows: RouteRow[] = useMemo(() => (metrics || []).map((m: RouteMetrics) => ({ id: `${m.appId}/${m.routeId}`, routeId: m.routeId, appId: m.appId, exchangeCount: m.exchangeCount, successRate: m.successRate, avgDurationMs: m.avgDurationMs, p99DurationMs: m.p99DurationMs, errorRate: m.errorRate, throughputPerSec: m.throughputPerSec, sparkline: m.sparkline ?? [], })), [metrics], ); // Sparkline data from timeseries buckets const throughputSparkline = useMemo(() => (timeseries?.buckets || []).map((b) => b.totalCount), [timeseries], ); const errorSparkline = useMemo(() => (timeseries?.buckets || []).map((b) => b.failedCount), [timeseries], ); // Chart series from timeseries buckets const throughputChartSeries = useMemo(() => [{ label: 'Throughput', data: (timeseries?.buckets || []).map((b, i) => ({ x: i as number, y: b.totalCount, })), }], [timeseries]); const latencyChartSeries = useMemo(() => [{ label: 'Latency', data: (timeseries?.buckets || []).map((b, i) => ({ x: i as number, y: b.avgDurationMs, })), }], [timeseries]); const errorBarSeries = useMemo(() => [{ label: 'Errors', data: (timeseries?.buckets || []).map((b) => { const ts = new Date(b.time); const label = !isNaN(ts.getTime()) ? ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—'; return { x: label, y: b.failedCount }; }), }], [timeseries]); const volumeChartSeries = useMemo(() => [{ label: 'Volume', data: (timeseries?.buckets || []).map((b, i) => ({ x: i as number, y: b.totalCount, })), }], [timeseries]); const kpiItems = useMemo(() => buildKpiItems(stats, rows.length, throughputSparkline, errorSparkline), [stats, rows.length, throughputSparkline, errorSparkline], ); return (
Auto-refresh: 30s
{/* KPI header cards */} {/* Per-route performance table */}
Per-Route Performance
{rows.length} routes
{ const targetAppId = appId ?? row.appId; navigate(`/routes/${targetAppId}/${row.routeId}`); }} />
{/* 2x2 chart grid */} {(timeseries?.buckets?.length ?? 0) > 0 && (
)}
); }