feat: migrate agent charts to ThemedChart + Recharts
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:
@@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user