feat: replace UI with design system example pages wired to real API
Migrate all page components from the @cameleer/design-system v0.0.3 example UI, replacing mock data with real backend API hooks. This brings richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline, DateRangePicker, expandable rows) while preserving all existing API integration, auth, and routing infrastructure. Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail, AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles). Also enhanced LayoutShell CommandPalette with real search data from catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,31 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router';
|
||||
import {
|
||||
Badge, StatusDot, DataTable, Tabs,
|
||||
AreaChart, LineChart, BarChart, RouteFlow, Spinner,
|
||||
KpiStrip,
|
||||
Badge,
|
||||
StatusDot,
|
||||
DataTable,
|
||||
Tabs,
|
||||
AreaChart,
|
||||
LineChart,
|
||||
BarChart,
|
||||
RouteFlow,
|
||||
Spinner,
|
||||
MonoText,
|
||||
Sparkline,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column } from '@cameleer/design-system';
|
||||
import type { KpiItem, Column } from '@cameleer/design-system';
|
||||
import { useGlobalFilters } from '@cameleer/design-system';
|
||||
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
|
||||
import { useStatsTimeseries, useSearchExecutions } from '../../api/queries/executions';
|
||||
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
||||
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
|
||||
import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping';
|
||||
import styles from './RouteDetail.module.css';
|
||||
|
||||
// ── Row types ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ExchangeRow extends ExecutionSummary {
|
||||
id: string;
|
||||
}
|
||||
@@ -26,6 +37,8 @@ interface ProcessorRow {
|
||||
avgDurationMs: number;
|
||||
p99DurationMs: number;
|
||||
errorCount: number;
|
||||
errorRate: number;
|
||||
sparkline: number[];
|
||||
}
|
||||
|
||||
interface ErrorPattern {
|
||||
@@ -34,6 +47,211 @@ interface ErrorPattern {
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
// ── Processor type badge classes ─────────────────────────────────────────────
|
||||
|
||||
const TYPE_STYLE_MAP: Record<string, string> = {
|
||||
consumer: styles.typeConsumer,
|
||||
producer: styles.typeProducer,
|
||||
enricher: styles.typeEnricher,
|
||||
validator: styles.typeValidator,
|
||||
transformer: styles.typeTransformer,
|
||||
router: styles.typeRouter,
|
||||
processor: styles.typeProcessor,
|
||||
};
|
||||
|
||||
function classifyProcessorType(processorId: string): string {
|
||||
const lower = processorId.toLowerCase();
|
||||
if (lower.startsWith('from(') || lower.includes('consumer')) return 'consumer';
|
||||
if (lower.startsWith('to(')) return 'producer';
|
||||
if (lower.includes('enrich')) return 'enricher';
|
||||
if (lower.includes('validate') || lower.includes('check')) return 'validator';
|
||||
if (lower.includes('unmarshal') || lower.includes('marshal')) return 'transformer';
|
||||
if (lower.includes('route') || lower.includes('choice')) return 'router';
|
||||
return 'processor';
|
||||
}
|
||||
|
||||
// ── Processor table columns ──────────────────────────────────────────────────
|
||||
|
||||
function makeProcessorColumns(css: typeof styles): Column<ProcessorRow>[] {
|
||||
return [
|
||||
{
|
||||
key: 'processorId',
|
||||
header: 'Processor',
|
||||
sortable: true,
|
||||
render: (_, row) => (
|
||||
<span className={css.routeNameCell}>{row.processorId}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'callCount',
|
||||
header: 'Invocations',
|
||||
sortable: true,
|
||||
render: (_, row) => (
|
||||
<MonoText size="sm">{row.callCount.toLocaleString()}</MonoText>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'avgDurationMs',
|
||||
header: 'Avg Duration',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.avgDurationMs > 200 ? css.rateBad : row.avgDurationMs > 100 ? css.rateWarn : css.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.avgDurationMs)}ms</MonoText>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'p99DurationMs',
|
||||
header: 'p99 Duration',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.p99DurationMs > 300 ? css.rateBad : row.p99DurationMs > 200 ? css.rateWarn : css.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.p99DurationMs)}ms</MonoText>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'errorCount',
|
||||
header: 'Errors',
|
||||
sortable: true,
|
||||
render: (_, row) => (
|
||||
<MonoText size="sm" className={row.errorCount > 10 ? css.rateBad : css.rateNeutral}>
|
||||
{row.errorCount}
|
||||
</MonoText>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'errorRate',
|
||||
header: 'Error Rate',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.errorRate > 1 ? css.rateBad : row.errorRate > 0.5 ? css.rateWarn : css.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{row.errorRate.toFixed(2)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'sparkline',
|
||||
header: 'Trend',
|
||||
render: (_, row) => (
|
||||
<Sparkline data={row.sparkline} width={80} height={24} />
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ── Exchange table columns ───────────────────────────────────────────────────
|
||||
|
||||
const EXCHANGE_COLUMNS: Column<ExchangeRow>[] = [
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
width: '80px',
|
||||
render: (_, row) => (
|
||||
<StatusDot variant={row.status === 'COMPLETED' ? 'success' : row.status === 'FAILED' ? 'error' : 'running'} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'executionId',
|
||||
header: 'Exchange ID',
|
||||
render: (_, row) => <MonoText size="xs">{row.executionId.slice(0, 12)}</MonoText>,
|
||||
},
|
||||
{
|
||||
key: 'startTime',
|
||||
header: 'Started',
|
||||
sortable: true,
|
||||
render: (_, row) => new Date(row.startTime).toLocaleTimeString(),
|
||||
},
|
||||
{
|
||||
key: 'durationMs',
|
||||
header: 'Duration',
|
||||
sortable: true,
|
||||
render: (_, row) => `${row.durationMs}ms`,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Build KPI items ──────────────────────────────────────────────────────────
|
||||
|
||||
function buildDetailKpiItems(
|
||||
stats: {
|
||||
totalCount: number;
|
||||
failedCount: number;
|
||||
avgDurationMs: number;
|
||||
p99LatencyMs: number;
|
||||
activeCount: number;
|
||||
prevTotalCount: number;
|
||||
prevFailedCount: number;
|
||||
prevP99LatencyMs: number;
|
||||
} | undefined,
|
||||
throughputSparkline: number[],
|
||||
errorSparkline: number[],
|
||||
latencySparkline: number[],
|
||||
): KpiItem[] {
|
||||
const totalCount = stats?.totalCount ?? 0;
|
||||
const failedCount = stats?.failedCount ?? 0;
|
||||
const prevTotalCount = stats?.prevTotalCount ?? 0;
|
||||
const p99Ms = stats?.p99LatencyMs ?? 0;
|
||||
const prevP99Ms = stats?.prevP99LatencyMs ?? 0;
|
||||
const avgMs = stats?.avgDurationMs ?? 0;
|
||||
const activeCount = stats?.activeCount ?? 0;
|
||||
|
||||
const errorRate = totalCount > 0 ? (failedCount / totalCount) * 100 : 0;
|
||||
const successRate = totalCount > 0 ? ((totalCount - failedCount) / totalCount) * 100 : 100;
|
||||
|
||||
const throughputPctChange = prevTotalCount > 0
|
||||
? Math.round(((totalCount - prevTotalCount) / prevTotalCount) * 100)
|
||||
: 0;
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Total Throughput',
|
||||
value: totalCount.toLocaleString(),
|
||||
trend: {
|
||||
label: throughputPctChange >= 0 ? `\u25B2 +${throughputPctChange}%` : `\u25BC ${throughputPctChange}%`,
|
||||
variant: throughputPctChange >= 0 ? 'success' as const : 'error' as const,
|
||||
},
|
||||
subtitle: `${activeCount} in-flight`,
|
||||
sparkline: throughputSparkline,
|
||||
borderColor: 'var(--amber)',
|
||||
},
|
||||
{
|
||||
label: 'System Error Rate',
|
||||
value: `${errorRate.toFixed(2)}%`,
|
||||
trend: {
|
||||
label: errorRate < 1 ? '\u25BC low' : `\u25B2 ${errorRate.toFixed(1)}%`,
|
||||
variant: errorRate < 1 ? 'success' as const : 'error' as const,
|
||||
},
|
||||
subtitle: `${failedCount} errors / ${totalCount.toLocaleString()} total`,
|
||||
sparkline: errorSparkline,
|
||||
borderColor: errorRate < 1 ? 'var(--success)' : 'var(--error)',
|
||||
},
|
||||
{
|
||||
label: 'Latency P99',
|
||||
value: `${p99Ms}ms`,
|
||||
trend: {
|
||||
label: p99Ms > prevP99Ms ? `\u25B2 +${p99Ms - prevP99Ms}ms` : `\u25BC ${prevP99Ms - p99Ms}ms`,
|
||||
variant: p99Ms > 300 ? 'error' as const : 'warning' as const,
|
||||
},
|
||||
subtitle: `Avg ${avgMs}ms \u00B7 SLA <300ms`,
|
||||
sparkline: latencySparkline,
|
||||
borderColor: p99Ms > 300 ? 'var(--warning)' : 'var(--success)',
|
||||
},
|
||||
{
|
||||
label: 'Success Rate',
|
||||
value: `${successRate.toFixed(1)}%`,
|
||||
trend: { label: '\u2194', variant: 'muted' as const },
|
||||
subtitle: `${totalCount - failedCount} ok / ${failedCount} failed`,
|
||||
borderColor: 'var(--success)',
|
||||
},
|
||||
{
|
||||
label: 'In-Flight',
|
||||
value: String(activeCount),
|
||||
trend: { label: '\u2194', variant: 'muted' as const },
|
||||
subtitle: `${activeCount} active exchanges`,
|
||||
borderColor: 'var(--amber)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function RouteDetail() {
|
||||
const { appId, routeId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -43,9 +261,11 @@ export default function RouteDetail() {
|
||||
|
||||
const [activeTab, setActiveTab] = useState('performance');
|
||||
|
||||
// ── API queries ────────────────────────────────────────────────────────────
|
||||
const { data: catalog } = useRouteCatalog();
|
||||
const { data: diagram } = useDiagramByRoute(appId, routeId);
|
||||
const { data: processorMetrics, isLoading: processorLoading } = useProcessorMetrics(routeId ?? null, appId);
|
||||
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId);
|
||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId);
|
||||
const { data: recentResult, isLoading: recentLoading } = useSearchExecutions({
|
||||
timeFrom,
|
||||
@@ -65,6 +285,8 @@ export default function RouteDetail() {
|
||||
limit: 200,
|
||||
});
|
||||
|
||||
// ── Derived data ───────────────────────────────────────────────────────────
|
||||
|
||||
const appEntry: AppCatalogEntry | undefined = useMemo(() =>
|
||||
(catalog || []).find((e: AppCatalogEntry) => e.appId === appId),
|
||||
[catalog, appId],
|
||||
@@ -79,7 +301,7 @@ export default function RouteDetail() {
|
||||
const exchangeCount = routeSummary?.exchangeCount ?? 0;
|
||||
const lastSeen = routeSummary?.lastSeen
|
||||
? new Date(routeSummary.lastSeen).toLocaleString()
|
||||
: '—';
|
||||
: '\u2014';
|
||||
|
||||
const healthVariant = useMemo((): 'success' | 'warning' | 'error' | 'dead' => {
|
||||
const h = health.toLowerCase();
|
||||
@@ -89,39 +311,70 @@ export default function RouteDetail() {
|
||||
return 'dead';
|
||||
}, [health]);
|
||||
|
||||
// Route flow from diagram
|
||||
const diagramNodes = useMemo(() => {
|
||||
if (!diagram?.nodes) return [];
|
||||
return mapDiagramToRouteNodes(diagram.nodes, []);
|
||||
}, [diagram]);
|
||||
|
||||
// Processor table rows
|
||||
const processorRows: ProcessorRow[] = useMemo(() =>
|
||||
(processorMetrics || []).map((p: any) => ({
|
||||
id: p.processorId,
|
||||
processorId: p.processorId,
|
||||
callCount: p.callCount ?? 0,
|
||||
avgDurationMs: p.avgDurationMs ?? 0,
|
||||
p99DurationMs: p.p99DurationMs ?? 0,
|
||||
errorCount: p.errorCount ?? 0,
|
||||
})),
|
||||
(processorMetrics || []).map((p: any) => {
|
||||
const callCount = p.callCount ?? 0;
|
||||
const errorCount = p.errorCount ?? 0;
|
||||
const errRate = callCount > 0 ? (errorCount / callCount) * 100 : 0;
|
||||
return {
|
||||
id: p.processorId,
|
||||
processorId: p.processorId,
|
||||
type: classifyProcessorType(p.processorId ?? ''),
|
||||
callCount,
|
||||
avgDurationMs: p.avgDurationMs ?? 0,
|
||||
p99DurationMs: p.p99DurationMs ?? 0,
|
||||
errorCount,
|
||||
errorRate: Number(errRate.toFixed(2)),
|
||||
sparkline: p.sparkline ?? [],
|
||||
};
|
||||
}),
|
||||
[processorMetrics],
|
||||
);
|
||||
|
||||
const chartData = useMemo(() =>
|
||||
(timeseries?.buckets || []).map((b: any) => ({
|
||||
time: new Date(b.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
throughput: b.totalCount,
|
||||
latency: b.avgDurationMs,
|
||||
errors: b.failedCount,
|
||||
successRate: b.totalCount > 0 ? ((b.totalCount - b.failedCount) / b.totalCount) * 100 : 100,
|
||||
})),
|
||||
// Timeseries-derived data
|
||||
const throughputSparkline = useMemo(() =>
|
||||
(timeseries?.buckets || []).map((b) => b.totalCount),
|
||||
[timeseries],
|
||||
);
|
||||
const errorSparkline = useMemo(() =>
|
||||
(timeseries?.buckets || []).map((b) => b.failedCount),
|
||||
[timeseries],
|
||||
);
|
||||
const latencySparkline = useMemo(() =>
|
||||
(timeseries?.buckets || []).map((b) => b.p99DurationMs),
|
||||
[timeseries],
|
||||
);
|
||||
|
||||
const chartData = useMemo(() =>
|
||||
(timeseries?.buckets || []).map((b) => {
|
||||
const ts = new Date(b.time);
|
||||
return {
|
||||
time: !isNaN(ts.getTime())
|
||||
? ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
: '\u2014',
|
||||
throughput: b.totalCount,
|
||||
latency: b.avgDurationMs,
|
||||
errors: b.failedCount,
|
||||
successRate: b.totalCount > 0 ? ((b.totalCount - b.failedCount) / b.totalCount) * 100 : 100,
|
||||
};
|
||||
}),
|
||||
[timeseries],
|
||||
);
|
||||
|
||||
// Exchange rows
|
||||
const exchangeRows: ExchangeRow[] = useMemo(() =>
|
||||
(recentResult?.data || []).map((e: ExecutionSummary) => ({ ...e, id: e.executionId })),
|
||||
[recentResult],
|
||||
);
|
||||
|
||||
// Error patterns
|
||||
const errorPatterns: ErrorPattern[] = useMemo(() => {
|
||||
const failed = (errorResult?.data || []) as ExecutionSummary[];
|
||||
const grouped = new Map<string, { count: number; lastSeen: string }>();
|
||||
@@ -141,31 +394,18 @@ export default function RouteDetail() {
|
||||
.map(([message, { count, lastSeen: ls }]) => ({
|
||||
message,
|
||||
count,
|
||||
lastSeen: ls ? new Date(ls).toLocaleString() : '—',
|
||||
lastSeen: ls ? new Date(ls).toLocaleString() : '\u2014',
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}, [errorResult]);
|
||||
|
||||
const processorColumns: Column<ProcessorRow>[] = [
|
||||
{ key: 'processorId', header: 'Processor', render: (v) => <MonoText size="sm">{String(v)}</MonoText> },
|
||||
{ key: 'callCount', header: 'Calls', sortable: true },
|
||||
{ key: 'avgDurationMs', header: 'Avg', sortable: true, render: (v) => `${(v as number).toFixed(1)}ms` },
|
||||
{ key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(1)}ms` },
|
||||
{ key: 'errorCount', header: 'Errors', sortable: true, render: (v) => {
|
||||
const n = v as number;
|
||||
return n > 0 ? <span style={{ color: 'var(--error)', fontWeight: 700 }}>{n}</span> : <span>0</span>;
|
||||
}},
|
||||
];
|
||||
// KPI items
|
||||
const kpiItems = useMemo(() =>
|
||||
buildDetailKpiItems(stats, throughputSparkline, errorSparkline, latencySparkline),
|
||||
[stats, throughputSparkline, errorSparkline, latencySparkline],
|
||||
);
|
||||
|
||||
const exchangeColumns: Column<ExchangeRow>[] = [
|
||||
{
|
||||
key: 'status', header: 'Status', width: '80px',
|
||||
render: (v) => <StatusDot variant={v === 'COMPLETED' ? 'success' : v === 'FAILED' ? 'error' : 'running'} />,
|
||||
},
|
||||
{ key: 'executionId', header: 'Exchange ID', render: (v) => <MonoText size="xs">{String(v).slice(0, 12)}</MonoText> },
|
||||
{ key: 'startTime', header: 'Started', sortable: true, render: (v) => new Date(v as string).toLocaleTimeString() },
|
||||
{ key: 'durationMs', header: 'Duration', sortable: true, render: (v) => `${v}ms` },
|
||||
];
|
||||
const processorColumns = useMemo(() => makeProcessorColumns(styles), []);
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Performance', value: 'performance' },
|
||||
@@ -173,12 +413,15 @@ export default function RouteDetail() {
|
||||
{ label: 'Error Patterns', value: 'errors', count: errorPatterns.length },
|
||||
];
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link to={`/routes/${appId}`} className={styles.backLink}>
|
||||
← {appId} routes
|
||||
← {appId} routes
|
||||
</Link>
|
||||
|
||||
{/* Route header card */}
|
||||
<div className={styles.headerCard}>
|
||||
<div className={styles.headerRow}>
|
||||
<div className={styles.headerLeft}>
|
||||
@@ -199,13 +442,17 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI strip */}
|
||||
<KpiStrip items={kpiItems} />
|
||||
|
||||
{/* Diagram + Processor Stats grid */}
|
||||
<div className={styles.diagramStatsGrid}>
|
||||
<div className={styles.diagramPane}>
|
||||
<div className={styles.paneTitle}>Route Diagram</div>
|
||||
{diagramNodes.length > 0 ? (
|
||||
<RouteFlow nodes={diagramNodes} />
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 13, padding: '8px 0' }}>
|
||||
<div className={styles.emptyText}>
|
||||
No diagram available for this route.
|
||||
</div>
|
||||
)}
|
||||
@@ -217,13 +464,40 @@ export default function RouteDetail() {
|
||||
) : processorRows.length > 0 ? (
|
||||
<DataTable columns={processorColumns} data={processorRows} sortable pageSize={10} />
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 13, padding: '8px 0' }}>
|
||||
<div className={styles.emptyText}>
|
||||
No processor data available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Processor Performance table (full width) */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Processor Performance</span>
|
||||
<div className={styles.tableRight}>
|
||||
<span className={styles.tableMeta}>{processorRows.length} processors</span>
|
||||
<Badge label="LIVE" color="success" />
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={processorColumns}
|
||||
data={processorRows}
|
||||
sortable
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Route Flow section */}
|
||||
{diagramNodes.length > 0 && (
|
||||
<div className={styles.routeFlowSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Route Flow</span>
|
||||
</div>
|
||||
<RouteFlow nodes={diagramNodes} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabbed section: Performance charts, Recent Executions, Error Patterns */}
|
||||
<div className={styles.tabSection}>
|
||||
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
|
||||
|
||||
@@ -232,28 +506,41 @@ export default function RouteDetail() {
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartTitle}>Throughput</div>
|
||||
<AreaChart
|
||||
series={[{ label: 'Throughput', data: chartData.map((d, i) => ({ x: i, y: d.throughput })) }]}
|
||||
series={[{
|
||||
label: 'Throughput',
|
||||
data: chartData.map((d, i) => ({ x: i, y: d.throughput })),
|
||||
}]}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartTitle}>Latency</div>
|
||||
<LineChart
|
||||
series={[{ label: 'Latency', data: chartData.map((d, i) => ({ x: i, y: d.latency })) }]}
|
||||
series={[{
|
||||
label: 'Latency',
|
||||
data: chartData.map((d, i) => ({ x: i, y: d.latency })),
|
||||
}]}
|
||||
height={200}
|
||||
threshold={{ value: 300, label: 'SLA 300ms' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartTitle}>Errors</div>
|
||||
<BarChart
|
||||
series={[{ label: 'Errors', data: chartData.map((d) => ({ x: d.time, y: d.errors })) }]}
|
||||
series={[{
|
||||
label: 'Errors',
|
||||
data: chartData.map((d) => ({ x: d.time, y: d.errors })),
|
||||
}]}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartTitle}>Success Rate</div>
|
||||
<AreaChart
|
||||
series={[{ label: 'Success Rate', data: chartData.map((d, i) => ({ x: i, y: d.successRate })) }]}
|
||||
series={[{
|
||||
label: 'Success Rate',
|
||||
data: chartData.map((d, i) => ({ x: i, y: d.successRate })),
|
||||
}]}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
@@ -268,7 +555,7 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
columns={exchangeColumns}
|
||||
columns={EXCHANGE_COLUMNS}
|
||||
data={exchangeRows}
|
||||
onRowClick={(row) => navigate(`/exchanges/${row.executionId}`)}
|
||||
sortable
|
||||
@@ -281,7 +568,7 @@ export default function RouteDetail() {
|
||||
{activeTab === 'errors' && (
|
||||
<div className={styles.errorPatterns} style={{ marginTop: 16 }}>
|
||||
{errorPatterns.length === 0 ? (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 13, padding: '8px 0' }}>
|
||||
<div className={styles.emptyText}>
|
||||
No error patterns found in the selected time range.
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user