import { useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { KpiStrip, DataTable, AreaChart, LineChart, 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 { useTimeseriesByRoute, useTopErrors, useAppSettings, usePunchcard, } from '../../api/queries/dashboard'; import type { TopError } from '../../api/queries/dashboard'; import { Treemap } from './Treemap'; import type { TreemapItem } from './Treemap'; import { PunchcardHeatmap } from './PunchcardHeatmap'; import type { RouteMetrics } from '../../api/types'; import { trendArrow, trendIndicator, formatThroughput, formatSlaCompliance, formatRelativeTime, } from './dashboard-utils'; import styles from './DashboardTab.module.css'; // ── Route table row type ──────────────────────────────────────────────────── interface RouteRow { id: string; routeId: string; exchangeCount: number; successRate: number; avgDurationMs: number; p99DurationMs: number; slaCompliance: number; sparkline: number[]; } // ── Route performance columns ─────────────────────────────────────────────── const ROUTE_COLUMNS: Column[] = [ { key: 'routeId', header: 'Route ID', sortable: true, render: (_, row) => ( {row.routeId} ), }, { key: 'exchangeCount', header: 'Throughput', 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(ms)', sortable: true, render: (_, row) => ( {Math.round(row.avgDurationMs)} ), }, { key: 'p99DurationMs', header: 'P99(ms)', sortable: true, render: (_, row) => { const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood; return {Math.round(row.p99DurationMs)}; }, }, { key: 'slaCompliance', header: 'SLA%', sortable: true, render: (_, row) => { const cls = row.slaCompliance >= 99 ? styles.rateGood : row.slaCompliance >= 95 ? styles.rateWarn : styles.rateBad; return {formatSlaCompliance(row.slaCompliance)}; }, }, { key: 'sparkline', header: 'Sparkline', render: (_, row) => ( ), }, ]; // ── Top errors columns ────────────────────────────────────────────────────── const ERROR_COLUMNS: Column[] = [ { key: 'errorType', header: 'Error Type', sortable: true, render: (_, row) => ( {row.errorType} ), }, { key: 'routeId', header: 'Route', sortable: true, render: (_, row) => ( {row.routeId ?? '\u2014'} ), }, { key: 'count', header: 'Count', sortable: true, render: (_, row) => ( {row.count.toLocaleString()} ), }, { key: 'velocity', header: 'Velocity', sortable: true, render: (_, row) => { const arrow = trendArrow(row.trend); const cls = row.trend === 'accelerating' ? styles.rateBad : row.trend === 'decelerating' ? styles.rateGood : styles.rateNeutral; return {row.velocity.toFixed(1)}/min {arrow}; }, }, { key: 'lastSeen', header: 'Last Seen', sortable: true, render: (_, row) => ( {formatRelativeTime(row.lastSeen)} ), }, ]; // ── Build KPI items ───────────────────────────────────────────────────────── function buildKpiItems( stats: { totalCount: number; failedCount: number; p99LatencyMs: number; prevTotalCount: number; prevFailedCount: number; prevP99LatencyMs: number; } | undefined, slaThresholdMs: number, throughputSparkline: number[], latencySparkline: number[], errors: TopError[] | undefined, windowSeconds: number, ): KpiItem[] { const totalCount = stats?.totalCount ?? 0; const failedCount = stats?.failedCount ?? 0; const prevTotalCount = stats?.prevTotalCount ?? 0; const prevFailedCount = stats?.prevFailedCount ?? 0; const p99Ms = stats?.p99LatencyMs ?? 0; const prevP99Ms = stats?.prevP99LatencyMs ?? 0; // Throughput const throughputTrend = trendIndicator(totalCount, prevTotalCount); // Success Rate const successRate = totalCount > 0 ? ((totalCount - failedCount) / totalCount) * 100 : 100; const prevSuccessRate = prevTotalCount > 0 ? ((prevTotalCount - prevFailedCount) / prevTotalCount) * 100 : 100; const successTrend = trendIndicator(successRate, prevSuccessRate); // P99 Latency const latencyTrend = trendIndicator(p99Ms, prevP99Ms); // SLA Compliance — percentage of exchanges under threshold // Approximate from p99: if p99 < threshold, ~99%+ are compliant const slaCompliance = p99Ms <= slaThresholdMs ? 99.9 : Math.max(0, 100 - ((p99Ms - slaThresholdMs) / slaThresholdMs) * 10); // Error Velocity — aggregate from top errors const errorList = errors ?? []; const totalVelocity = errorList.reduce((sum, e) => sum + e.velocity, 0); const hasAccelerating = errorList.some((e) => e.trend === 'accelerating'); const allDecelerating = errorList.length > 0 && errorList.every((e) => e.trend === 'decelerating'); const velocityTrendLabel = hasAccelerating ? '\u25B2' : allDecelerating ? '\u25BC' : '\u2500\u2500'; const velocityVariant = hasAccelerating ? 'error' as const : allDecelerating ? 'success' as const : 'muted' as const; return [ { label: 'Throughput', value: formatThroughput(totalCount, windowSeconds), trend: { label: throughputTrend.label, variant: throughputTrend.direction === 'up' ? 'success' as const : throughputTrend.direction === 'down' ? 'error' as const : 'muted' as const, }, sparkline: throughputSparkline, borderColor: 'var(--amber)', }, { label: 'Success Rate', value: `${successRate.toFixed(2)}%`, trend: { label: successTrend.label, variant: successTrend.direction === 'up' ? 'success' as const : successTrend.direction === 'down' ? 'error' as const : 'muted' as const, }, borderColor: successRate >= 99 ? 'var(--success)' : successRate >= 95 ? 'var(--warning)' : 'var(--error)', }, { label: 'P99 Latency', value: `${Math.round(p99Ms)}ms`, trend: { label: latencyTrend.label, variant: latencyTrend.direction === 'up' ? 'error' as const : latencyTrend.direction === 'down' ? 'success' as const : 'muted' as const, }, sparkline: latencySparkline, borderColor: p99Ms > slaThresholdMs ? 'var(--error)' : 'var(--success)', }, { label: 'SLA Compliance', value: formatSlaCompliance(slaCompliance), trend: { label: slaCompliance >= 99 ? 'OK' : 'BREACH', variant: slaCompliance >= 99 ? 'success' as const : 'error' as const, }, subtitle: `Threshold: ${slaThresholdMs}ms`, borderColor: slaCompliance >= 99 ? 'var(--success)' : slaCompliance >= 95 ? 'var(--warning)' : 'var(--error)', }, { label: 'Error Velocity', value: `${totalVelocity.toFixed(1)}/min`, trend: { label: velocityTrendLabel, variant: velocityVariant, }, subtitle: `${errorList.length} error type${errorList.length !== 1 ? 's' : ''} tracked`, borderColor: hasAccelerating ? 'var(--error)' : allDecelerating ? 'var(--success)' : 'var(--text-muted)', }, ]; } // ── Component ─────────────────────────────────────────────────────────────── export default function DashboardL2() { const { appId } = useParams<{ appId: string }>(); const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000; // Data hooks const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId); const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId); const { data: errors } = useTopErrors(timeFrom, timeTo, appId); const { data: punchcardData } = usePunchcard(appId); const { data: appSettings } = useAppSettings(appId); const slaThresholdMs = appSettings?.slaThresholdMs ?? 300; // Route performance table rows const routeRows: RouteRow[] = useMemo(() => (metrics || []).map((m: RouteMetrics) => ({ id: m.routeId, routeId: m.routeId, exchangeCount: m.exchangeCount, successRate: m.successRate, avgDurationMs: m.avgDurationMs, p99DurationMs: m.p99DurationMs, slaCompliance: (m as Record).slaCompliance as number ?? -1, sparkline: m.sparkline ?? [], })), [metrics], ); // Treemap items: one per route, sized by exchange count, colored by SLA const treemapItems: TreemapItem[] = useMemo( () => routeRows.map(r => ({ id: r.routeId, label: r.routeId, value: r.exchangeCount, slaCompliance: r.slaCompliance >= 0 ? r.slaCompliance : 100, })), [routeRows], ); // KPI sparklines from timeseries const throughputSparkline = useMemo(() => (timeseries?.buckets || []).map((b) => b.totalCount), [timeseries], ); const latencySparkline = useMemo(() => (timeseries?.buckets || []).map((b) => b.p99DurationMs), [timeseries], ); const kpiItems = useMemo(() => buildKpiItems(stats, slaThresholdMs, throughputSparkline, latencySparkline, errors, windowSeconds), [stats, slaThresholdMs, throughputSparkline, latencySparkline, errors, windowSeconds], ); // Throughput by Route — stacked area chart series const throughputByRouteSeries = useMemo(() => { if (!timeseriesByRoute) return []; return Object.entries(timeseriesByRoute).map(([routeId, data]) => ({ label: routeId, data: (data.buckets || []).map((b, i) => ({ x: i as number, y: b.totalCount, })), })); }, [timeseriesByRoute]); // Latency percentiles chart — P99 line from app-level timeseries const latencyChartSeries = useMemo(() => { const buckets = timeseries?.buckets || []; return [ { label: 'P99', data: buckets.map((b, i) => ({ x: i as number, y: b.p99DurationMs, })), }, { label: 'Avg', data: buckets.map((b, i) => ({ x: i as number, y: b.avgDurationMs, })), }, ]; }, [timeseries]); // Error rows with stable identity const errorRows = useMemo(() => (errors ?? []).map((e, i) => ({ ...e, id: `${e.errorType}-${e.routeId}-${i}` })), [errors], ); return (
Auto-refresh: 30s
{/* KPI Strip */} {/* Route Performance Table */}
Route Performance
{routeRows.length} routes
navigate(`/dashboard/${appId}/${row.routeId}`)} />
{/* Charts: Throughput by Route + Latency Percentiles */} {(timeseries?.buckets?.length ?? 0) > 0 && (
)} {/* Top 5 Errors — hidden when empty */} {errorRows.length > 0 && (
Top Errors {errorRows.length} error types
)} {/* Treemap + Punchcard heatmaps side by side */} {treemapItems.length > 0 && (
navigate(`/dashboard/${appId}/${id}`)} />
)}
); }