diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx index be84453c..98de8471 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.tsx +++ b/ui/src/pages/AgentInstance/AgentInstance.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { useParams, Link } from 'react-router'; import { RefreshCw, ChevronRight } from 'lucide-react'; import { - StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart, + StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS, EventFeed, Spinner, EmptyState, SectionHeader, MonoText, LogViewer, ButtonGroup, useGlobalFilters, } from '@cameleer/design-system'; @@ -100,46 +100,42 @@ export default function AgentInstance() { return eventSortAsc ? mapped.toReversed() : mapped; }, [events, instanceId, eventSortAsc]); - // JVM chart series helpers - const cpuSeries = useMemo(() => { + const formatTime = (t: string) => + new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + const cpuData = useMemo(() => { const pts = jvmMetrics?.metrics?.['process.cpu.usage.value']; - if (!pts?.length) return null; - return [{ label: 'CPU %', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value * 100 })) }]; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, cpu: p.value * 100 })); }, [jvmMetrics]); - const heapSeries = useMemo(() => { + const heapData = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.memory.used.value']; - if (!pts?.length) return null; - return [{ label: 'Heap MB', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value / (1024 * 1024) })) }]; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, heap: p.value / (1024 * 1024) })); }, [jvmMetrics]); - const threadSeries = useMemo(() => { + const threadData = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.threads.live.value']; - if (!pts?.length) return null; - return [{ label: 'Threads', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }]; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, threads: p.value })); }, [jvmMetrics]); - const gcSeries = useMemo(() => { + const gcData = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.gc.pause.total_time']; - if (!pts?.length) return null; - return [{ label: 'GC ms', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }]; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, gc: p.value })); }, [jvmMetrics]); - const throughputSeries = useMemo( - () => - chartData.length - ? [{ label: 'msg/s', data: chartData.map((d: any) => ({ x: d.date, y: d.throughput })) }] - : null, - [chartData], - ); + const throughputData = useMemo(() => { + if (!chartData.length) return []; + return chartData.map((d: any) => ({ time: d.date.toISOString(), throughput: d.throughput })); + }, [chartData]); - const errorSeries = useMemo( - () => - chartData.length - ? [{ label: 'Error %', data: chartData.map((d: any) => ({ x: d.date, y: d.errorPct })) }] - : null, - [chartData], - ); + const errorData = useMemo(() => { + if (!chartData.length) return []; + return chartData.map((d: any) => ({ time: d.date.toISOString(), errorPct: d.errorPct })); + }, [chartData]); // Application logs const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined }); @@ -315,13 +311,14 @@ export default function AgentInstance() { {cpuDisplay != null ? `${cpuDisplay}% current` : ''} - {cpuSeries ? ( - + {cpuData.length ? ( + + + + ) : ( )} @@ -336,13 +333,16 @@ export default function AgentInstance() { : ''} - {heapSeries ? ( - + {heapData.length ? ( + + + {heapMax != null && ( + + )} + ) : ( )} @@ -355,8 +355,12 @@ export default function AgentInstance() { {agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''} - {throughputSeries ? ( - + {throughputData.length ? ( + + + ) : ( )} @@ -369,8 +373,12 @@ export default function AgentInstance() { {agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''} - {errorSeries ? ( - + {errorData.length ? ( + + + ) : ( )} @@ -380,13 +388,17 @@ export default function AgentInstance() {
Thread Count - {threadSeries - ? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active` + {threadData.length + ? `${threadData[threadData.length - 1].threads.toFixed(0)} active` : ''}
- {threadSeries ? ( - + {threadData.length ? ( + + + ) : ( )} @@ -397,8 +409,12 @@ export default function AgentInstance() { GC Pauses - {gcSeries ? ( - + {gcData.length ? ( + + + ) : ( )} diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx index d0468f18..4b3c353e 100644 --- a/ui/src/pages/DashboardTab/DashboardL1.tsx +++ b/ui/src/pages/DashboardTab/DashboardL1.tsx @@ -3,8 +3,10 @@ import { useNavigate } from 'react-router'; import { KpiStrip, DataTable, - AreaChart, - LineChart, + ThemedChart, + Area, + Line, + CHART_COLORS, Card, Sparkline, MonoText, @@ -372,28 +374,39 @@ export default function DashboardL1() { throughputSparkline, successSparkline, latencySparkline, slaSparkline, errorSparkline], ); - // ── Per-app chart series (throughput stacked area) ────────────────────── - const throughputByAppSeries = useMemo(() => { - if (!timeseriesByApp) return []; - return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({ - label: appId, - data: buckets.map((b, i) => ({ - x: i as number, - y: b.totalCount, - })), - })); + // ── Per-app flat chart data (throughput stacked area) ─────────────────── + const throughputByAppData = useMemo(() => { + if (!timeseriesByApp) return { data: [], keys: [] as string[] }; + const entries = Object.entries(timeseriesByApp); + if (!entries.length) return { data: [], keys: [] as string[] }; + const len = entries[0][1].buckets.length; + const keys = entries.map(([appId]) => appId); + const data = Array.from({ length: len }, (_, i) => { + const point: Record = { idx: i }; + for (const [appId, { buckets }] of entries) { + point[appId] = buckets[i]?.totalCount ?? 0; + } + return point; + }); + return { data, keys }; }, [timeseriesByApp]); - // ── Per-app chart series (error rate line) ───────────────────────────── - const errorRateByAppSeries = useMemo(() => { - if (!timeseriesByApp) return []; - return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({ - label: appId, - data: buckets.map((b, i) => ({ - x: i as number, - y: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0, - })), - })); + // ── Per-app flat chart data (error rate line) ──────────────────────────── + const errorRateByAppData = useMemo(() => { + if (!timeseriesByApp) return { data: [], keys: [] as string[] }; + const entries = Object.entries(timeseriesByApp); + if (!entries.length) return { data: [], keys: [] as string[] }; + const len = entries[0][1].buckets.length; + const keys = entries.map(([appId]) => appId); + const data = Array.from({ length: len }, (_, i) => { + const point: Record = { idx: i }; + for (const [appId, { buckets }] of entries) { + const b = buckets[i]; + point[appId] = b && b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0; + } + return point; + }); + return { data, keys }; }, [timeseriesByApp]); // Treemap items: one per app, sized by exchange count, colored by SLA @@ -430,24 +443,24 @@ export default function DashboardL1() { {/* Side-by-side charts */} - {throughputByAppSeries.length > 0 && ( + {throughputByAppData.data.length > 0 && (
- + + {throughputByAppData.keys.map((key, i) => ( + + ))} + - + + {errorRateByAppData.keys.map((key, i) => ( + + ))} +
)} diff --git a/ui/src/pages/DashboardTab/DashboardL2.tsx b/ui/src/pages/DashboardTab/DashboardL2.tsx index aef35faa..e0f1fa45 100644 --- a/ui/src/pages/DashboardTab/DashboardL2.tsx +++ b/ui/src/pages/DashboardTab/DashboardL2.tsx @@ -3,8 +3,11 @@ import { useParams, useNavigate } from 'react-router'; import { KpiStrip, DataTable, - AreaChart, - LineChart, + ThemedChart, + Area, + Line, + ReferenceLine, + CHART_COLORS, Card, Sparkline, MonoText, @@ -328,37 +331,31 @@ export default function DashboardL2() { [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, - })), - })); + // Throughput by Route — flat data for ThemedChart + const throughputByRouteData = useMemo(() => { + if (!timeseriesByRoute) return { data: [], keys: [] as string[] }; + const entries = Object.entries(timeseriesByRoute); + if (!entries.length) return { data: [], keys: [] as string[] }; + const len = entries[0][1].buckets.length; + const keys = entries.map(([routeId]) => routeId); + const data = Array.from({ length: len }, (_, i) => { + const point: Record = { idx: i }; + for (const [routeId, { buckets }] of entries) { + point[routeId] = buckets[i]?.totalCount ?? 0; + } + return point; + }); + return { data, keys }; }, [timeseriesByRoute]); - // Latency percentiles chart — P99 line from app-level timeseries - const latencyChartSeries = useMemo(() => { + // Latency percentiles chart — flat data for ThemedChart + const latencyChartData = 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, - })), - }, - ]; + return buckets.map((b, i) => ({ + idx: i, + p99: b.p99DurationMs, + avg: b.avgDurationMs, + })); }, [timeseries]); // Error rows with stable identity @@ -398,22 +395,21 @@ export default function DashboardL2() { {(timeseries?.buckets?.length ?? 0) > 0 && (
- + + {throughputByRouteData.keys.map((key, i) => ( + + ))} + - + + + + +
)} diff --git a/ui/src/pages/DashboardTab/DashboardL3.tsx b/ui/src/pages/DashboardTab/DashboardL3.tsx index 47b7f181..139d2909 100644 --- a/ui/src/pages/DashboardTab/DashboardL3.tsx +++ b/ui/src/pages/DashboardTab/DashboardL3.tsx @@ -3,8 +3,11 @@ import { useParams } from 'react-router'; import { KpiStrip, DataTable, - AreaChart, - LineChart, + ThemedChart, + Area, + Line, + ReferenceLine, + CHART_COLORS, Card, MonoText, Badge, @@ -285,31 +288,16 @@ export default function DashboardL3() { [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, + // ── Chart data ─────────────────────────────────────────────────────────── + const chartData = useMemo(() => + (timeseries?.buckets || []).map((b: any, i: number) => ({ + idx: i, + throughput: b.totalCount, + p99: b.p99DurationMs, + errorRate: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0, })), - }], [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]); + [timeseries], + ); // ── Processor table rows ──────────────────────────────────────────────── const processorRows: ProcessorRow[] = useMemo(() => { @@ -364,28 +352,25 @@ export default function DashboardL3() { {(timeseries?.buckets?.length ?? 0) > 0 && (
- + + + - + + + + - + + +
)} diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx index 1f9637ee..ee9a64ca 100644 --- a/ui/src/pages/Routes/RouteDetail.tsx +++ b/ui/src/pages/Routes/RouteDetail.tsx @@ -8,9 +8,12 @@ import { DataTable, EmptyState, Tabs, - AreaChart, - LineChart, - BarChart, + ThemedChart, + Area, + Line, + Bar, + ReferenceLine, + CHART_COLORS, RouteFlow, Spinner, MonoText, @@ -752,44 +755,31 @@ export default function RouteDetail() {
Throughput
- ({ x: i, y: d.throughput })), - }]} - height={200} - /> + + +
Latency
- ({ x: i, y: d.latency })), - }]} - height={200} - threshold={{ value: 300, label: 'SLA 300ms' }} - /> + + + +
Errors
- ({ x: d.time, y: d.errors })), - }]} - height={200} - /> + + +
Success Rate
- ({ x: i, y: d.successRate })), - }]} - height={200} - /> + + +
)} diff --git a/ui/src/pages/Routes/RoutesMetrics.tsx b/ui/src/pages/Routes/RoutesMetrics.tsx index 4019351e..96d70edb 100644 --- a/ui/src/pages/Routes/RoutesMetrics.tsx +++ b/ui/src/pages/Routes/RoutesMetrics.tsx @@ -3,9 +3,12 @@ import { useParams, useNavigate } from 'react-router'; import { KpiStrip, DataTable, - AreaChart, - LineChart, - BarChart, + ThemedChart, + Area, + Line, + Bar, + ReferenceLine, + CHART_COLORS, Card, Sparkline, MonoText, @@ -243,41 +246,17 @@ export default function RoutesMetrics() { [timeseries], ); - // Chart series from timeseries buckets - const throughputChartSeries = useMemo(() => [{ - label: 'Throughput', - data: (timeseries?.buckets || []).map((b, i) => ({ - x: i as number, - y: b.totalCount, + // Flat chart data from timeseries buckets + const chartData = useMemo(() => + (timeseries?.buckets || []).map((b, i) => ({ + idx: i, + throughput: b.totalCount, + latency: b.avgDurationMs, + errors: b.failedCount, + volume: 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]); + [timeseries], + ); const kpiItems = useMemo(() => buildKpiItems(stats, rows.length, throughputSparkline, errorSparkline), @@ -315,42 +294,34 @@ export default function RoutesMetrics() { {/* 2x2 chart grid */} - {(timeseries?.buckets?.length ?? 0) > 0 && ( + {chartData.length > 0 && (
- + + + - + + + + - - + + + + - + + +
)}