feat: migrate agent charts to ThemedChart + Recharts
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled

Replace custom LineChart/AreaChart/BarChart usage with ThemedChart
wrapper. Data format changed from ChartSeries[] to Recharts-native
flat objects. Uses DS v0.1.47.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 19:44:55 +02:00
parent a0af53f8f5
commit 0dae1f1cc7
6 changed files with 238 additions and 267 deletions

View File

@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useParams, Link } from 'react-router'; import { useParams, Link } from 'react-router';
import { RefreshCw, ChevronRight } from 'lucide-react'; import { RefreshCw, ChevronRight } from 'lucide-react';
import { import {
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart, StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
EventFeed, Spinner, EmptyState, SectionHeader, MonoText, EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
LogViewer, ButtonGroup, useGlobalFilters, LogViewer, ButtonGroup, useGlobalFilters,
} from '@cameleer/design-system'; } from '@cameleer/design-system';
@@ -100,46 +100,42 @@ export default function AgentInstance() {
return eventSortAsc ? mapped.toReversed() : mapped; return eventSortAsc ? mapped.toReversed() : mapped;
}, [events, instanceId, eventSortAsc]); }, [events, instanceId, eventSortAsc]);
// JVM chart series helpers const formatTime = (t: string) =>
const cpuSeries = useMemo(() => { new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const cpuData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['process.cpu.usage.value']; const pts = jvmMetrics?.metrics?.['process.cpu.usage.value'];
if (!pts?.length) return null; if (!pts?.length) return [];
return [{ label: 'CPU %', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value * 100 })) }]; return pts.map((p: any) => ({ time: p.time, cpu: p.value * 100 }));
}, [jvmMetrics]); }, [jvmMetrics]);
const heapSeries = useMemo(() => { const heapData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.memory.used.value']; const pts = jvmMetrics?.metrics?.['jvm.memory.used.value'];
if (!pts?.length) return null; if (!pts?.length) return [];
return [{ label: 'Heap MB', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value / (1024 * 1024) })) }]; return pts.map((p: any) => ({ time: p.time, heap: p.value / (1024 * 1024) }));
}, [jvmMetrics]); }, [jvmMetrics]);
const threadSeries = useMemo(() => { const threadData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.threads.live.value']; const pts = jvmMetrics?.metrics?.['jvm.threads.live.value'];
if (!pts?.length) return null; if (!pts?.length) return [];
return [{ label: 'Threads', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }]; return pts.map((p: any) => ({ time: p.time, threads: p.value }));
}, [jvmMetrics]); }, [jvmMetrics]);
const gcSeries = useMemo(() => { const gcData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.gc.pause.total_time']; const pts = jvmMetrics?.metrics?.['jvm.gc.pause.total_time'];
if (!pts?.length) return null; if (!pts?.length) return [];
return [{ label: 'GC ms', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }]; return pts.map((p: any) => ({ time: p.time, gc: p.value }));
}, [jvmMetrics]); }, [jvmMetrics]);
const throughputSeries = useMemo( const throughputData = useMemo(() => {
() => if (!chartData.length) return [];
chartData.length return chartData.map((d: any) => ({ time: d.date.toISOString(), throughput: d.throughput }));
? [{ label: 'msg/s', data: chartData.map((d: any) => ({ x: d.date, y: d.throughput })) }] }, [chartData]);
: null,
[chartData],
);
const errorSeries = useMemo( const errorData = useMemo(() => {
() => if (!chartData.length) return [];
chartData.length return chartData.map((d: any) => ({ time: d.date.toISOString(), errorPct: d.errorPct }));
? [{ label: 'Error %', data: chartData.map((d: any) => ({ x: d.date, y: d.errorPct })) }] }, [chartData]);
: null,
[chartData],
);
// Application logs // Application logs
const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined }); const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined });
@@ -315,13 +311,14 @@ export default function AgentInstance() {
{cpuDisplay != null ? `${cpuDisplay}% current` : ''} {cpuDisplay != null ? `${cpuDisplay}% current` : ''}
</span> </span>
</div> </div>
{cpuSeries ? ( {cpuData.length ? (
<AreaChart <ThemedChart data={cpuData} height={160} xDataKey="time"
series={cpuSeries} xTickFormatter={formatTime} yLabel="%">
height={160} <Area dataKey="cpu" name="CPU %" stroke={CHART_COLORS[0]}
yLabel="%" fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
threshold={{ value: 85, label: 'Alert' }} <ReferenceLine y={85} stroke="var(--error)" strokeDasharray="5 3"
/> label={{ value: 'Alert', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No CPU metrics available" /> <EmptyState title="No data" description="No CPU metrics available" />
)} )}
@@ -336,13 +333,16 @@ export default function AgentInstance() {
: ''} : ''}
</span> </span>
</div> </div>
{heapSeries ? ( {heapData.length ? (
<AreaChart <ThemedChart data={heapData} height={160} xDataKey="time"
series={heapSeries} xTickFormatter={formatTime} yLabel="MB">
height={160} <Area dataKey="heap" name="Heap MB" stroke={CHART_COLORS[0]}
yLabel="MB" fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
threshold={heapMax != null ? { value: heapMax / (1024 * 1024), label: 'Max Heap' } : undefined} {heapMax != null && (
/> <ReferenceLine y={heapMax / (1024 * 1024)} stroke="var(--error)" strokeDasharray="5 3"
label={{ value: 'Max Heap', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
)}
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No heap metrics available" /> <EmptyState title="No data" description="No heap metrics available" />
)} )}
@@ -355,8 +355,12 @@ export default function AgentInstance() {
{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''} {agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
</span> </span>
</div> </div>
{throughputSeries ? ( {throughputData.length ? (
<LineChart series={throughputSeries} height={160} yLabel="msg/s" /> <ThemedChart data={throughputData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="msg/s">
<Line dataKey="throughput" name="msg/s" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No throughput data in range" /> <EmptyState title="No data" description="No throughput data in range" />
)} )}
@@ -369,8 +373,12 @@ export default function AgentInstance() {
{agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''} {agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}
</span> </span>
</div> </div>
{errorSeries ? ( {errorData.length ? (
<LineChart series={errorSeries} height={160} yLabel="%" /> <ThemedChart data={errorData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="%">
<Line dataKey="errorPct" name="Error %" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No error data in range" /> <EmptyState title="No data" description="No error data in range" />
)} )}
@@ -380,13 +388,17 @@ export default function AgentInstance() {
<div className={styles.chartHeader}> <div className={styles.chartHeader}>
<span className={styles.chartTitle}>Thread Count</span> <span className={styles.chartTitle}>Thread Count</span>
<span className={styles.chartMeta}> <span className={styles.chartMeta}>
{threadSeries {threadData.length
? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active` ? `${threadData[threadData.length - 1].threads.toFixed(0)} active`
: ''} : ''}
</span> </span>
</div> </div>
{threadSeries ? ( {threadData.length ? (
<LineChart series={threadSeries} height={160} yLabel="threads" /> <ThemedChart data={threadData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="threads">
<Line dataKey="threads" name="Threads" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No thread metrics available" /> <EmptyState title="No data" description="No thread metrics available" />
)} )}
@@ -397,8 +409,12 @@ export default function AgentInstance() {
<span className={styles.chartTitle}>GC Pauses</span> <span className={styles.chartTitle}>GC Pauses</span>
<span className={styles.chartMeta} /> <span className={styles.chartMeta} />
</div> </div>
{gcSeries ? ( {gcData.length ? (
<AreaChart series={gcSeries} height={160} yLabel="ms" /> <ThemedChart data={gcData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="ms">
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
</ThemedChart>
) : ( ) : (
<EmptyState title="No data" description="No GC metrics available" /> <EmptyState title="No data" description="No GC metrics available" />
)} )}

View File

@@ -3,8 +3,10 @@ import { useNavigate } from 'react-router';
import { import {
KpiStrip, KpiStrip,
DataTable, DataTable,
AreaChart, ThemedChart,
LineChart, Area,
Line,
CHART_COLORS,
Card, Card,
Sparkline, Sparkline,
MonoText, MonoText,
@@ -372,28 +374,39 @@ export default function DashboardL1() {
throughputSparkline, successSparkline, latencySparkline, slaSparkline, errorSparkline], throughputSparkline, successSparkline, latencySparkline, slaSparkline, errorSparkline],
); );
// ── Per-app chart series (throughput stacked area) ────────────────────── // ── Per-app flat chart data (throughput stacked area) ───────────────────
const throughputByAppSeries = useMemo(() => { const throughputByAppData = useMemo(() => {
if (!timeseriesByApp) return []; if (!timeseriesByApp) return { data: [], keys: [] as string[] };
return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({ const entries = Object.entries(timeseriesByApp);
label: appId, if (!entries.length) return { data: [], keys: [] as string[] };
data: buckets.map((b, i) => ({ const len = entries[0][1].buckets.length;
x: i as number, const keys = entries.map(([appId]) => appId);
y: b.totalCount, const data = Array.from({ length: len }, (_, i) => {
})), const point: Record<string, number | string> = { idx: i };
})); for (const [appId, { buckets }] of entries) {
point[appId] = buckets[i]?.totalCount ?? 0;
}
return point;
});
return { data, keys };
}, [timeseriesByApp]); }, [timeseriesByApp]);
// ── Per-app chart series (error rate line) ──────────────────────────── // ── Per-app flat chart data (error rate line) ────────────────────────────
const errorRateByAppSeries = useMemo(() => { const errorRateByAppData = useMemo(() => {
if (!timeseriesByApp) return []; if (!timeseriesByApp) return { data: [], keys: [] as string[] };
return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({ const entries = Object.entries(timeseriesByApp);
label: appId, if (!entries.length) return { data: [], keys: [] as string[] };
data: buckets.map((b, i) => ({ const len = entries[0][1].buckets.length;
x: i as number, const keys = entries.map(([appId]) => appId);
y: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0, const data = Array.from({ length: len }, (_, i) => {
})), const point: Record<string, number | string> = { 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]); }, [timeseriesByApp]);
// Treemap items: one per app, sized by exchange count, colored by SLA // Treemap items: one per app, sized by exchange count, colored by SLA
@@ -430,24 +443,24 @@ export default function DashboardL1() {
</div> </div>
{/* Side-by-side charts */} {/* Side-by-side charts */}
{throughputByAppSeries.length > 0 && ( {throughputByAppData.data.length > 0 && (
<div className={styles.chartGrid}> <div className={styles.chartGrid}>
<Card title="Throughput by Application (msg/s)"> <Card title="Throughput by Application (msg/s)">
<AreaChart <ThemedChart data={throughputByAppData.data} height={200} xDataKey="idx" yLabel="msg/s">
series={throughputByAppSeries} {throughputByAppData.keys.map((key, i) => (
yLabel="msg/s" <Area key={key} dataKey={key} name={key} stroke={CHART_COLORS[i % CHART_COLORS.length]}
height={200} fill={CHART_COLORS[i % CHART_COLORS.length]} fillOpacity={0.1} strokeWidth={2} dot={false} />
className={styles.chart} ))}
/> </ThemedChart>
</Card> </Card>
<Card title="Error Rate by Application (%)"> <Card title="Error Rate by Application (%)">
<LineChart <ThemedChart data={errorRateByAppData.data} height={200} xDataKey="idx" yLabel="%">
series={errorRateByAppSeries} {errorRateByAppData.keys.map((key, i) => (
yLabel="%" <Line key={key} dataKey={key} name={key} stroke={CHART_COLORS[i % CHART_COLORS.length]}
height={200} strokeWidth={2} dot={false} />
className={styles.chart} ))}
/> </ThemedChart>
</Card> </Card>
</div> </div>
)} )}

View File

@@ -3,8 +3,11 @@ import { useParams, useNavigate } from 'react-router';
import { import {
KpiStrip, KpiStrip,
DataTable, DataTable,
AreaChart, ThemedChart,
LineChart, Area,
Line,
ReferenceLine,
CHART_COLORS,
Card, Card,
Sparkline, Sparkline,
MonoText, MonoText,
@@ -328,37 +331,31 @@ export default function DashboardL2() {
[stats, slaThresholdMs, throughputSparkline, latencySparkline, errors, windowSeconds], [stats, slaThresholdMs, throughputSparkline, latencySparkline, errors, windowSeconds],
); );
// Throughput by Route — stacked area chart series // Throughput by Route — flat data for ThemedChart
const throughputByRouteSeries = useMemo(() => { const throughputByRouteData = useMemo(() => {
if (!timeseriesByRoute) return []; if (!timeseriesByRoute) return { data: [], keys: [] as string[] };
return Object.entries(timeseriesByRoute).map(([routeId, data]) => ({ const entries = Object.entries(timeseriesByRoute);
label: routeId, if (!entries.length) return { data: [], keys: [] as string[] };
data: (data.buckets || []).map((b, i) => ({ const len = entries[0][1].buckets.length;
x: i as number, const keys = entries.map(([routeId]) => routeId);
y: b.totalCount, const data = Array.from({ length: len }, (_, i) => {
})), const point: Record<string, number | string> = { idx: i };
})); for (const [routeId, { buckets }] of entries) {
point[routeId] = buckets[i]?.totalCount ?? 0;
}
return point;
});
return { data, keys };
}, [timeseriesByRoute]); }, [timeseriesByRoute]);
// Latency percentiles chart — P99 line from app-level timeseries // Latency percentiles chart — flat data for ThemedChart
const latencyChartSeries = useMemo(() => { const latencyChartData = useMemo(() => {
const buckets = timeseries?.buckets || []; const buckets = timeseries?.buckets || [];
return [ return buckets.map((b, i) => ({
{ idx: i,
label: 'P99', p99: b.p99DurationMs,
data: buckets.map((b, i) => ({ avg: b.avgDurationMs,
x: i as number, }));
y: b.p99DurationMs,
})),
},
{
label: 'Avg',
data: buckets.map((b, i) => ({
x: i as number,
y: b.avgDurationMs,
})),
},
];
}, [timeseries]); }, [timeseries]);
// Error rows with stable identity // Error rows with stable identity
@@ -398,22 +395,21 @@ export default function DashboardL2() {
{(timeseries?.buckets?.length ?? 0) > 0 && ( {(timeseries?.buckets?.length ?? 0) > 0 && (
<div className={styles.chartGrid}> <div className={styles.chartGrid}>
<Card title="Throughput by Route"> <Card title="Throughput by Route">
<AreaChart <ThemedChart data={throughputByRouteData.data} height={200} xDataKey="idx" yLabel="msg/s">
series={throughputByRouteSeries} {throughputByRouteData.keys.map((key, i) => (
yLabel="msg/s" <Area key={key} dataKey={key} name={key} stroke={CHART_COLORS[i % CHART_COLORS.length]}
height={200} fill={CHART_COLORS[i % CHART_COLORS.length]} fillOpacity={0.1} strokeWidth={2} dot={false} />
className={styles.chart} ))}
/> </ThemedChart>
</Card> </Card>
<Card title="Latency Percentiles"> <Card title="Latency Percentiles">
<LineChart <ThemedChart data={latencyChartData} height={200} xDataKey="idx" yLabel="ms">
series={latencyChartSeries} <Line dataKey="p99" name="P99" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
yLabel="ms" <Line dataKey="avg" name="Avg" stroke={CHART_COLORS[1]} strokeWidth={2} dot={false} />
threshold={{ value: slaThresholdMs, label: `SLA ${slaThresholdMs}ms` }} <ReferenceLine y={slaThresholdMs} stroke="var(--error)" strokeDasharray="5 3"
height={200} label={{ value: `SLA ${slaThresholdMs}ms`, position: 'right', fill: 'var(--error)', fontSize: 9 }} />
className={styles.chart} </ThemedChart>
/>
</Card> </Card>
</div> </div>
)} )}

View File

@@ -3,8 +3,11 @@ import { useParams } from 'react-router';
import { import {
KpiStrip, KpiStrip,
DataTable, DataTable,
AreaChart, ThemedChart,
LineChart, Area,
Line,
ReferenceLine,
CHART_COLORS,
Card, Card,
MonoText, MonoText,
Badge, Badge,
@@ -285,31 +288,16 @@ export default function DashboardL3() {
[stats, slaThresholdMs, bottleneck, throughputSparkline, windowSeconds], [stats, slaThresholdMs, bottleneck, throughputSparkline, windowSeconds],
); );
// ── Chart series ──────────────────────────────────────────────────────── // ── Chart data ───────────────────────────────────────────────────────────
const throughputChartSeries = useMemo(() => [{ const chartData = useMemo(() =>
label: 'Throughput', (timeseries?.buckets || []).map((b: any, i: number) => ({
data: (timeseries?.buckets || []).map((b: any, i: number) => ({ idx: i,
x: i, throughput: b.totalCount,
y: b.totalCount, p99: b.p99DurationMs,
errorRate: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0,
})), })),
}], [timeseries]); [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 ──────────────────────────────────────────────── // ── Processor table rows ────────────────────────────────────────────────
const processorRows: ProcessorRow[] = useMemo(() => { const processorRows: ProcessorRow[] = useMemo(() => {
@@ -364,28 +352,25 @@ export default function DashboardL3() {
{(timeseries?.buckets?.length ?? 0) > 0 && ( {(timeseries?.buckets?.length ?? 0) > 0 && (
<div className={styles.chartRow}> <div className={styles.chartRow}>
<Card title="Throughput"> <Card title="Throughput">
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="msg/s">
series={throughputChartSeries} <Area dataKey="throughput" name="Throughput" stroke={CHART_COLORS[0]}
yLabel="msg/s" fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
height={200} </ThemedChart>
/>
</Card> </Card>
<Card title="Latency Percentiles"> <Card title="Latency Percentiles">
<LineChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="ms">
series={latencyChartSeries} <Line dataKey="p99" name="P99" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
yLabel="ms" <ReferenceLine y={slaThresholdMs} stroke="var(--error)" strokeDasharray="5 3"
threshold={{ value: slaThresholdMs, label: `SLA ${slaThresholdMs}ms` }} label={{ value: `SLA ${slaThresholdMs}ms`, position: 'right', fill: 'var(--error)', fontSize: 9 }} />
height={200} </ThemedChart>
/>
</Card> </Card>
<Card title="Error Rate"> <Card title="Error Rate">
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="%">
series={errorRateChartSeries} <Area dataKey="errorRate" name="Error Rate" stroke={CHART_COLORS[1]}
yLabel="%" fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
height={200} </ThemedChart>
/>
</Card> </Card>
</div> </div>
)} )}

View File

@@ -8,9 +8,12 @@ import {
DataTable, DataTable,
EmptyState, EmptyState,
Tabs, Tabs,
AreaChart, ThemedChart,
LineChart, Area,
BarChart, Line,
Bar,
ReferenceLine,
CHART_COLORS,
RouteFlow, RouteFlow,
Spinner, Spinner,
MonoText, MonoText,
@@ -752,44 +755,31 @@ export default function RouteDetail() {
<div className={styles.chartGrid} style={{ marginTop: 16 }}> <div className={styles.chartGrid} style={{ marginTop: 16 }}>
<div className={chartCardStyles.chartCard}> <div className={chartCardStyles.chartCard}>
<div className={styles.chartTitle}>Throughput</div> <div className={styles.chartTitle}>Throughput</div>
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="time" yLabel="msg/s">
series={[{ <Area dataKey="throughput" name="Throughput" stroke={CHART_COLORS[0]}
label: 'Throughput', fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
data: chartData.map((d, i) => ({ x: i, y: d.throughput })), </ThemedChart>
}]}
height={200}
/>
</div> </div>
<div className={chartCardStyles.chartCard}> <div className={chartCardStyles.chartCard}>
<div className={styles.chartTitle}>Latency</div> <div className={styles.chartTitle}>Latency</div>
<LineChart <ThemedChart data={chartData} height={200} xDataKey="time" yLabel="ms">
series={[{ <Line dataKey="latency" name="Latency" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
label: 'Latency', <ReferenceLine y={300} stroke="var(--error)" strokeDasharray="5 3"
data: chartData.map((d, i) => ({ x: i, y: d.latency })), label={{ value: 'SLA 300ms', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
}]} </ThemedChart>
height={200}
threshold={{ value: 300, label: 'SLA 300ms' }}
/>
</div> </div>
<div className={chartCardStyles.chartCard}> <div className={chartCardStyles.chartCard}>
<div className={styles.chartTitle}>Errors</div> <div className={styles.chartTitle}>Errors</div>
<BarChart <ThemedChart data={chartData} height={200} xDataKey="time" yLabel="errors">
series={[{ <Bar dataKey="errors" name="Errors" fill={CHART_COLORS[1]} />
label: 'Errors', </ThemedChart>
data: chartData.map((d) => ({ x: d.time, y: d.errors })),
}]}
height={200}
/>
</div> </div>
<div className={chartCardStyles.chartCard}> <div className={chartCardStyles.chartCard}>
<div className={styles.chartTitle}>Success Rate</div> <div className={styles.chartTitle}>Success Rate</div>
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="time" yLabel="%">
series={[{ <Area dataKey="successRate" name="Success Rate" stroke={CHART_COLORS[0]}
label: 'Success Rate', fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
data: chartData.map((d, i) => ({ x: i, y: d.successRate })), </ThemedChart>
}]}
height={200}
/>
</div> </div>
</div> </div>
)} )}

View File

@@ -3,9 +3,12 @@ import { useParams, useNavigate } from 'react-router';
import { import {
KpiStrip, KpiStrip,
DataTable, DataTable,
AreaChart, ThemedChart,
LineChart, Area,
BarChart, Line,
Bar,
ReferenceLine,
CHART_COLORS,
Card, Card,
Sparkline, Sparkline,
MonoText, MonoText,
@@ -243,41 +246,17 @@ export default function RoutesMetrics() {
[timeseries], [timeseries],
); );
// Chart series from timeseries buckets // Flat chart data from timeseries buckets
const throughputChartSeries = useMemo(() => [{ const chartData = useMemo(() =>
label: 'Throughput', (timeseries?.buckets || []).map((b, i) => ({
data: (timeseries?.buckets || []).map((b, i) => ({ idx: i,
x: i as number, throughput: b.totalCount,
y: b.totalCount, latency: b.avgDurationMs,
errors: b.failedCount,
volume: b.totalCount,
})), })),
}], [timeseries]); [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(() => const kpiItems = useMemo(() =>
buildKpiItems(stats, rows.length, throughputSparkline, errorSparkline), buildKpiItems(stats, rows.length, throughputSparkline, errorSparkline),
@@ -315,42 +294,34 @@ export default function RoutesMetrics() {
</div> </div>
{/* 2x2 chart grid */} {/* 2x2 chart grid */}
{(timeseries?.buckets?.length ?? 0) > 0 && ( {chartData.length > 0 && (
<div className={styles.chartGrid}> <div className={styles.chartGrid}>
<Card title="Throughput (msg/s)"> <Card title="Throughput (msg/s)">
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="msg/s">
series={throughputChartSeries} <Area dataKey="throughput" name="Throughput" stroke={CHART_COLORS[0]}
yLabel="msg/s" fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
height={200} </ThemedChart>
className={styles.chart}
/>
</Card> </Card>
<Card title="Latency (ms)"> <Card title="Latency (ms)">
<LineChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="ms">
series={latencyChartSeries} <Line dataKey="latency" name="Latency" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
yLabel="ms" <ReferenceLine y={300} stroke="var(--error)" strokeDasharray="5 3"
threshold={{ value: 300, label: 'SLA 300ms' }} label={{ value: 'SLA 300ms', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
height={200} </ThemedChart>
className={styles.chart}
/>
</Card> </Card>
<Card title="Errors by Route"> <Card title="Errors by Bucket">
<BarChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="errors">
series={errorBarSeries} <Bar dataKey="errors" name="Errors" fill={CHART_COLORS[1]} />
height={200} </ThemedChart>
className={styles.chart}
/>
</Card> </Card>
<Card title="Message Volume (msg/min)"> <Card title="Message Volume (msg/min)">
<AreaChart <ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="msg/min">
series={volumeChartSeries} <Area dataKey="volume" name="Volume" stroke={CHART_COLORS[0]}
yLabel="msg/min" fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
height={200} </ThemedChart>
className={styles.chart}
/>
</Card> </Card>
</div> </div>
)} )}