import { useMemo } from 'react'; import { useParams } from 'react-router'; import { KpiStrip, DataTable, AreaChart, LineChart, Card, MonoText, Badge, } from '@cameleer/design-system'; import type { KpiItem, Column } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { useProcessorMetrics } from '../../api/queries/processor-metrics'; import { useTopErrors, useAppSettings } from '../../api/queries/dashboard'; import type { TopError } from '../../api/queries/dashboard'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { ProcessDiagram } from '../../components/ProcessDiagram'; import { formatRelativeTime, trendArrow, formatThroughput, formatSlaCompliance, trendIndicator, } from './dashboard-utils'; import styles from './DashboardTab.module.css'; // ── Row types ─────────────────────────────────────────────────────────────── interface ProcessorRow { id: string; processorId: string; processorType: string; totalCount: number; avgDurationMs: number; p99DurationMs: number; errorRate: number; pctTime: number; } interface ErrorRow extends TopError { id: string; } // ── Processor table columns ───────────────────────────────────────────────── const PROCESSOR_COLUMNS: Column[] = [ { key: 'processorId', header: 'Processor ID', sortable: true, render: (_, row) => {row.processorId}, }, { key: 'processorType', header: 'Type', sortable: true, render: (_, row) => , }, { key: 'totalCount', header: 'Invocations', sortable: true, render: (_, row) => ( {row.totalCount.toLocaleString()} ), }, { 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: '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(2)}%; }, }, { key: 'pctTime', header: '% Time', sortable: true, render: (_, row) => ( {row.pctTime.toFixed(1)}% ), }, ]; // ── Error table columns ───────────────────────────────────────────────────── const ERROR_COLUMNS: Column[] = [ { key: 'errorType', header: 'Error Type', sortable: true, render: (_, row) => {row.errorType}, }, { key: 'processorId', header: 'Processor', sortable: true, render: (_, row) => ( {row.processorId ?? '\u2014'} ), }, { key: 'count', header: 'Count', sortable: true, render: (_, row) => ( {row.count.toLocaleString()} ), }, { key: 'trend', header: 'Velocity', render: (_, row) => ( {trendArrow(row.trend)} {row.trend} ), }, { key: 'lastSeen', header: 'Last Seen', sortable: true, render: (_, row) => ( {formatRelativeTime(row.lastSeen)} ), }, ]; // ── Build KPI items ───────────────────────────────────────────────────────── function buildKpiItems( stats: { totalCount: number; failedCount: number; avgDurationMs: number; p99LatencyMs: number; activeCount: number; prevTotalCount: number; prevFailedCount: number; prevP99LatencyMs: number; } | undefined, slaThresholdMs: number, bottleneck: { processorId: string; avgMs: number; pct: number } | null, throughputSparkline: number[], windowSeconds: number, ): KpiItem[] { const totalCount = stats?.totalCount ?? 0; const failedCount = stats?.failedCount ?? 0; const prevTotalCount = stats?.prevTotalCount ?? 0; const p99Ms = stats?.p99LatencyMs ?? 0; const avgMs = stats?.avgDurationMs ?? 0; const successRate = totalCount > 0 ? ((totalCount - failedCount) / totalCount) * 100 : 100; const slaCompliance = totalCount > 0 ? ((totalCount - failedCount) / totalCount) * 100 : 100; const throughputTrend = trendIndicator(totalCount, prevTotalCount); 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, }, subtitle: `${totalCount.toLocaleString()} total exchanges`, sparkline: throughputSparkline, borderColor: 'var(--amber)', }, { label: 'Success Rate', value: `${successRate.toFixed(2)}%`, trend: { label: failedCount > 0 ? `${failedCount} failed` : 'No errors', variant: successRate >= 99 ? 'success' as const : successRate >= 97 ? 'warning' as const : 'error' as const, }, subtitle: `${totalCount - failedCount} succeeded / ${totalCount.toLocaleString()} total`, borderColor: successRate >= 99 ? 'var(--success)' : 'var(--error)', }, { label: 'P99 Latency', value: `${Math.round(p99Ms)}ms`, trend: { label: p99Ms > slaThresholdMs ? 'BREACH' : 'OK', variant: p99Ms > slaThresholdMs ? 'error' as const : 'success' as const, }, subtitle: `SLA threshold: ${slaThresholdMs}ms \u00B7 Avg: ${Math.round(avgMs)}ms`, borderColor: p99Ms > slaThresholdMs ? 'var(--warning)' : 'var(--success)', }, { label: 'SLA Compliance', value: formatSlaCompliance(slaCompliance), trend: { label: slaCompliance >= 99.9 ? 'Excellent' : slaCompliance >= 99 ? 'Good' : 'Degraded', variant: slaCompliance >= 99 ? 'success' as const : slaCompliance >= 95 ? 'warning' as const : 'error' as const, }, subtitle: `Target: 99.9%`, borderColor: slaCompliance >= 99 ? 'var(--success)' : 'var(--warning)', }, { label: 'Bottleneck', value: bottleneck ? `${Math.round(bottleneck.avgMs)}ms` : '\u2014', trend: { label: bottleneck ? `${bottleneck.pct.toFixed(1)}% of total` : '\u2014', variant: bottleneck && bottleneck.pct > 50 ? 'error' as const : 'muted' as const, }, subtitle: bottleneck ? `${bottleneck.processorId} \u00B7 ${Math.round(bottleneck.avgMs)}ms \u00B7 ${bottleneck.pct.toFixed(1)}% of total` : 'No processor data', borderColor: 'var(--running)', }, ]; } // ── Component ─────────────────────────────────────────────────────────────── export default function DashboardL3() { const { appId, routeId } = useParams<{ appId: string; routeId: string }>(); 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, routeId, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId); const { data: processorMetrics } = useProcessorMetrics(routeId ?? null, appId); const { data: topErrors } = useTopErrors(timeFrom, timeTo, appId, routeId); const { data: diagramLayout } = useDiagramByRoute(appId, routeId); const { data: appSettings } = useAppSettings(appId); const slaThresholdMs = appSettings?.slaThresholdMs ?? 300; // ── Bottleneck (processor with highest avgDurationMs) ─────────────────── const bottleneck = useMemo(() => { if (!processorMetrics?.length) return null; const routeAvg = stats?.avgDurationMs ?? 0; const sorted = [...processorMetrics].sort( (a: any, b: any) => b.avgDurationMs - a.avgDurationMs, ); const top = sorted[0]; const pct = routeAvg > 0 ? (top.avgDurationMs / routeAvg) * 100 : 0; return { processorId: top.processorId, avgMs: top.avgDurationMs, pct }; }, [processorMetrics, stats]); // ── Sparklines from timeseries ────────────────────────────────────────── const throughputSparkline = useMemo( () => (timeseries?.buckets || []).map((b: any) => b.totalCount), [timeseries], ); // ── KPI strip ─────────────────────────────────────────────────────────── const kpiItems = useMemo( () => buildKpiItems(stats, slaThresholdMs, bottleneck, throughputSparkline, windowSeconds), [stats, slaThresholdMs, bottleneck, throughputSparkline, windowSeconds], ); // ── Chart series ──────────────────────────────────────────────────────── const throughputChartSeries = useMemo(() => [{ label: 'Throughput', data: (timeseries?.buckets || []).map((b: any, i: number) => ({ x: i, y: b.totalCount, })), }], [timeseries]); const latencyChartSeries = useMemo(() => [{ label: 'P99', data: (timeseries?.buckets || []).map((b: any, i: number) => ({ x: i, y: b.p99DurationMs, })), }], [timeseries]); const errorRateChartSeries = useMemo(() => [{ label: 'Error Rate', data: (timeseries?.buckets || []).map((b: any, i: number) => ({ x: i, y: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0, })), color: 'var(--error)', }], [timeseries]); // ── Processor table rows ──────────────────────────────────────────────── const processorRows: ProcessorRow[] = useMemo(() => { if (!processorMetrics?.length) return []; const routeAvg = stats?.avgDurationMs ?? 0; return processorMetrics.map((m: any) => ({ id: m.processorId, processorId: m.processorId, processorType: m.processorType, totalCount: m.totalCount, avgDurationMs: m.avgDurationMs, p99DurationMs: m.p99DurationMs, errorRate: m.errorRate, pctTime: routeAvg > 0 ? (m.avgDurationMs / routeAvg) * 100 : 0, })); }, [processorMetrics, stats]); // ── Latency heatmap for ProcessDiagram ────────────────────────────────── const latencyHeatmap = useMemo(() => { if (!processorMetrics?.length) return new Map(); const totalAvg = processorMetrics.reduce( (sum: number, m: any) => sum + m.avgDurationMs, 0, ); const map = new Map(); for (const m of processorMetrics) { map.set(m.processorId, { avgDurationMs: m.avgDurationMs, p99DurationMs: m.p99DurationMs, pctOfRoute: totalAvg > 0 ? (m.avgDurationMs / totalAvg) * 100 : 0, }); } return map; }, [processorMetrics]); // ── Error table rows ──────────────────────────────────────────────────── const errorRows: ErrorRow[] = useMemo( () => (topErrors || []).map((e, i) => ({ ...e, id: `${e.errorType}-${i}` })), [topErrors], ); return (
Auto-refresh: 30s
{/* KPI Strip */} {/* Charts — 3 in a row */} {(timeseries?.buckets?.length ?? 0) > 0 && (
)} {/* Process Diagram with Latency Heatmap */} {appId && routeId && (
)} {/* Processor Metrics Table */}
Processor Metrics
{processorRows.length} processor{processorRows.length !== 1 ? 's' : ''}
{/* Top 5 Errors — hidden if empty */} {errorRows.length > 0 && (
Top 5 Errors
)}
); }