style: add CSS modules to all pages matching design system mock layouts
Replace inline styles with semantic CSS module classes for proper visual structure: card wrappers with borders/shadows, grid layouts for stat strips and charts, section headers, and typography classes. Pages updated: Dashboard, ExchangeDetail, RoutesMetrics, AgentHealth, AgentInstance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
ui/src/pages/AgentHealth/AgentHealth.module.css
Normal file
66
ui/src/pages/AgentHealth/AgentHealth.module.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
.statStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instanceRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instanceRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instanceRow:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instanceName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instanceTps {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCardHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StatCard, StatusDot, Badge, MonoText,
|
StatCard, StatusDot, Badge, MonoText,
|
||||||
GroupCard, EventFeed,
|
GroupCard, EventFeed,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
|
import styles from './AgentHealth.module.css';
|
||||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||||
import { useRouteCatalog } from '../../api/queries/catalog';
|
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||||
|
|
||||||
@@ -46,14 +47,14 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div className={styles.statStrip}>
|
||||||
<StatCard label="Total Agents" value={totalAgents} />
|
<StatCard label="Total Agents" value={totalAgents} />
|
||||||
<StatCard label="Live" value={liveCount} accent="success" />
|
<StatCard label="Live" value={liveCount} accent="success" />
|
||||||
<StatCard label="Stale" value={staleCount} accent="warning" />
|
<StatCard label="Stale" value={staleCount} accent="warning" />
|
||||||
<StatCard label="Dead" value={deadCount} accent="error" />
|
<StatCard label="Dead" value={deadCount} accent="error" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '1rem', marginBottom: '1.5rem' }}>
|
<div className={styles.groupGrid}>
|
||||||
{Object.entries(apps).map(([group, groupAgents]) => (
|
{Object.entries(apps).map(([group, groupAgents]) => (
|
||||||
<GroupCard
|
<GroupCard
|
||||||
key={group}
|
key={group}
|
||||||
@@ -69,13 +70,13 @@ export default function AgentHealth() {
|
|||||||
{(groupAgents || []).map((agent: any) => (
|
{(groupAgents || []).map((agent: any) => (
|
||||||
<div
|
<div
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0', cursor: 'pointer' }}
|
className={styles.instanceRow}
|
||||||
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${group}/${agent.id}`); }}
|
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${group}/${agent.id}`); }}
|
||||||
>
|
>
|
||||||
<StatusDot variant={agent.status === 'LIVE' ? 'live' : agent.status === 'STALE' ? 'stale' : 'dead'} />
|
<StatusDot variant={agent.status === 'LIVE' ? 'live' : agent.status === 'STALE' ? 'stale' : 'dead'} />
|
||||||
<MonoText size="sm">{agent.name}</MonoText>
|
<span className={styles.instanceName}>{agent.name}</span>
|
||||||
<Badge label={agent.status} color={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'} />
|
<Badge label={agent.status} color={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'} />
|
||||||
{agent.tps > 0 && <span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>{agent.tps.toFixed(1)} tps</span>}
|
{agent.tps > 0 && <span className={styles.instanceTps}>{agent.tps.toFixed(1)} tps</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</GroupCard>
|
</GroupCard>
|
||||||
@@ -83,8 +84,8 @@ export default function AgentHealth() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{feedEvents.length > 0 && (
|
{feedEvents.length > 0 && (
|
||||||
<div>
|
<div className={styles.eventCard}>
|
||||||
<h3 style={{ marginBottom: '0.75rem' }}>Event Log</h3>
|
<div className={styles.eventCardHeader}>Event Log</div>
|
||||||
<EventFeed events={feedEvents} maxItems={100} />
|
<EventFeed events={feedEvents} maxItems={100} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
92
ui/src/pages/AgentInstance/AgentInstance.module.css
Normal file
92
ui/src/pages/AgentInstance/AgentInstance.module.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
.statStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentHeader h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routeBadges {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 420px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventCardHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText, Card,
|
StatCard, StatusDot, Badge,
|
||||||
LineChart, AreaChart, EventFeed, Breadcrumb, Spinner,
|
LineChart, AreaChart, EventFeed, Breadcrumb, Spinner,
|
||||||
SectionHeader, CodeBlock,
|
CodeBlock,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
|
import styles from './AgentInstance.module.css';
|
||||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||||
import { useStatsTimeseries } from '../../api/queries/executions';
|
import { useStatsTimeseries } from '../../api/queries/executions';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
@@ -59,21 +60,21 @@ export default function AgentInstance() {
|
|||||||
|
|
||||||
{agent && (
|
{agent && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '1rem 0' }}>
|
<div className={styles.agentHeader}>
|
||||||
<StatusDot variant={agent.status === 'LIVE' ? 'live' : agent.status === 'STALE' ? 'stale' : 'dead'} />
|
<StatusDot variant={agent.status === 'LIVE' ? 'live' : agent.status === 'STALE' ? 'stale' : 'dead'} />
|
||||||
<h2>{agent.name}</h2>
|
<h2>{agent.name}</h2>
|
||||||
<Badge label={agent.status} color={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'} />
|
<Badge label={agent.status} color={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div className={styles.statStrip}>
|
||||||
<StatCard label="TPS" value={agent.tps?.toFixed(1) ?? '0'} />
|
<StatCard label="TPS" value={agent.tps?.toFixed(1) ?? '0'} />
|
||||||
<StatCard label="Error Rate" value={agent.errorRate ? `${(agent.errorRate * 100).toFixed(1)}%` : '0%'} accent={agent.errorRate > 0.05 ? 'error' : undefined} />
|
<StatCard label="Error Rate" value={agent.errorRate ? `${(agent.errorRate * 100).toFixed(1)}%` : '0%'} accent={agent.errorRate > 0.05 ? 'error' : undefined} />
|
||||||
<StatCard label="Active Routes" value={`${agent.activeRoutes ?? 0}/${agent.totalRoutes ?? 0}`} />
|
<StatCard label="Active Routes" value={`${agent.activeRoutes ?? 0}/${agent.totalRoutes ?? 0}`} />
|
||||||
<StatCard label="Uptime" value={formatUptime(agent.uptimeSeconds ?? 0)} />
|
<StatCard label="Uptime" value={formatUptime(agent.uptimeSeconds ?? 0)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SectionHeader>Routes</SectionHeader>
|
<div className={styles.sectionTitle}>Routes</div>
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
|
<div className={styles.routeBadges}>
|
||||||
{(agent.routeIds || []).map((r: string) => (
|
{(agent.routeIds || []).map((r: string) => (
|
||||||
<Badge key={r} label={r} color="auto" />
|
<Badge key={r} label={r} color="auto" />
|
||||||
))}
|
))}
|
||||||
@@ -83,26 +84,31 @@ export default function AgentInstance() {
|
|||||||
|
|
||||||
{chartData.length > 0 && (
|
{chartData.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SectionHeader>Performance</SectionHeader>
|
<div className={styles.sectionTitle}>Performance</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1.5rem' }}>
|
<div className={styles.chartsGrid}>
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<div className={styles.chartHeader}><div className={styles.chartTitle}>Throughput</div></div>
|
||||||
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }]} height={200} />
|
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }]} height={200} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<div className={styles.chartHeader}><div className={styles.chartTitle}>Latency</div></div>
|
||||||
<LineChart series={[{ label: 'Latency', data: chartData.map((d: any, i: number) => ({ x: i, y: d.latency })) }]} height={200} />
|
<LineChart series={[{ label: 'Latency', data: chartData.map((d: any, i: number) => ({ x: i, y: d.latency })) }]} height={200} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{feedEvents.length > 0 && (
|
{feedEvents.length > 0 && (
|
||||||
<>
|
<div className={styles.eventCard}>
|
||||||
<SectionHeader>Events</SectionHeader>
|
<div className={styles.eventCardHeader}>Events</div>
|
||||||
<EventFeed events={feedEvents} maxItems={50} />
|
<EventFeed events={feedEvents} maxItems={50} />
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agent && (
|
{agent && (
|
||||||
<>
|
<>
|
||||||
<SectionHeader>Agent Info</SectionHeader>
|
<div className={styles.sectionTitle}>Agent Info</div>
|
||||||
<Card>
|
<div className={styles.infoCard}>
|
||||||
<div style={{ padding: '1rem' }}>
|
|
||||||
<CodeBlock content={JSON.stringify({
|
<CodeBlock content={JSON.stringify({
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
@@ -112,7 +118,6 @@ export default function AgentInstance() {
|
|||||||
routeIds: agent.routeIds,
|
routeIds: agent.routeIds,
|
||||||
}, null, 2)} />
|
}, null, 2)} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
ui/src/pages/Dashboard/Dashboard.module.css
Normal file
84
ui/src/pages/Dashboard/Dashboard.module.css
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
.healthStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableMeta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelSection {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelSection:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelSectionTitle {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overviewGrid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overviewRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overviewLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 90px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system';
|
|||||||
import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
import type { ExecutionSummary } from '../../api/types';
|
import type { ExecutionSummary } from '../../api/types';
|
||||||
|
import styles from './Dashboard.module.css';
|
||||||
|
|
||||||
interface Row extends ExecutionSummary { id: string }
|
interface Row extends ExecutionSummary { id: string }
|
||||||
|
|
||||||
@@ -61,12 +62,32 @@ export default function Dashboard() {
|
|||||||
{
|
{
|
||||||
label: 'Overview', value: 'overview',
|
label: 'Overview', value: 'overview',
|
||||||
content: (
|
content: (
|
||||||
<div style={{ display: 'grid', gap: '0.75rem', padding: '1rem' }}>
|
<div className={styles.panelSection}>
|
||||||
<div><strong>Execution ID:</strong> <MonoText size="sm">{detail.executionId}</MonoText></div>
|
<div className={styles.panelSectionTitle}>Details</div>
|
||||||
<div><strong>Status:</strong> <Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} /></div>
|
<div className={styles.overviewGrid}>
|
||||||
<div><strong>Route:</strong> {detail.routeId}</div>
|
<div className={styles.overviewRow}>
|
||||||
<div><strong>Duration:</strong> {detail.durationMs}ms</div>
|
<span className={styles.overviewLabel}>Exchange ID</span>
|
||||||
{detail.errorMessage && <div><strong>Error:</strong> {detail.errorMessage}</div>}
|
<MonoText size="sm">{detail.executionId}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Status</span>
|
||||||
|
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Route</span>
|
||||||
|
<span>{detail.routeId}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Duration</span>
|
||||||
|
<span>{detail.durationMs}ms</span>
|
||||||
|
</div>
|
||||||
|
{detail.errorMessage && (
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Error</span>
|
||||||
|
<span>{detail.errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -85,7 +106,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div className={styles.healthStrip}>
|
||||||
<StatCard label="Total Exchanges" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
<StatCard label="Total Exchanges" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
||||||
<StatCard label="Failed" value={stats?.failedCount ?? 0} accent="error" />
|
<StatCard label="Failed" value={stats?.failedCount ?? 0} accent="error" />
|
||||||
<StatCard label="Avg Duration" value={`${stats?.avgDurationMs ?? 0}ms`} />
|
<StatCard label="Avg Duration" value={`${stats?.avgDurationMs ?? 0}ms`} />
|
||||||
@@ -93,6 +114,13 @@ export default function Dashboard() {
|
|||||||
<StatCard label="Active" value={stats?.activeCount ?? 0} accent="running" />
|
<StatCard label="Active" value={stats?.activeCount ?? 0} accent="running" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.tableSection}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span className={styles.tableTitle}>Recent Exchanges</span>
|
||||||
|
<div className={styles.tableRight}>
|
||||||
|
<span className={styles.tableMeta}>{rows.length} results</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
@@ -101,6 +129,7 @@ export default function Dashboard() {
|
|||||||
sortable
|
sortable
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DetailPanel
|
<DetailPanel
|
||||||
open={!!selectedId}
|
open={!!selectedId}
|
||||||
|
|||||||
143
ui/src/pages/ExchangeDetail/ExchangeDetail.module.css
Normal file
143
ui/src/pages/ExchangeDetail/ExchangeDetail.module.css
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
.exchangeHeader {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerRight {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerStat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerStatLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerStatValue {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelineSection {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelineHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelineTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelineBody {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailSplit {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailPanel {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelBody {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerKvRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerKvRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerKey {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerValue {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
Card, Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||||
ProcessorTimeline, Breadcrumb, Spinner,
|
ProcessorTimeline, Breadcrumb, Spinner,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
||||||
|
import styles from './ExchangeDetail.module.css';
|
||||||
|
|
||||||
export default function ExchangeDetail() {
|
export default function ExchangeDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -43,45 +44,43 @@ export default function ExchangeDetail() {
|
|||||||
{ label: id?.slice(0, 12) || '' },
|
{ label: id?.slice(0, 12) || '' },
|
||||||
]} />
|
]} />
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', margin: '1.5rem 0' }}>
|
<div className={styles.exchangeHeader}>
|
||||||
<Card>
|
<div className={styles.headerRow}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.headerLeft}>
|
||||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Status</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.25rem' }}>
|
|
||||||
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : detail.status === 'FAILED' ? 'error' : 'running'} />
|
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : detail.status === 'FAILED' ? 'error' : 'running'} />
|
||||||
|
<div>
|
||||||
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
||||||
|
<MonoText>{id}</MonoText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className={styles.headerRight}>
|
||||||
<Card>
|
<div className={styles.headerStat}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.headerStatLabel}>Duration</div>
|
||||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Duration</div>
|
<div className={styles.headerStatValue}>{detail.durationMs}ms</div>
|
||||||
<div style={{ fontSize: '1.25rem', fontWeight: 600 }}>{detail.durationMs}ms</div>
|
</div>
|
||||||
|
<div className={styles.headerStat}>
|
||||||
|
<div className={styles.headerStatLabel}>Route</div>
|
||||||
|
<div className={styles.headerStatValue}>{detail.routeId}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerStat}>
|
||||||
|
<div className={styles.headerStatLabel}>Application</div>
|
||||||
|
<div className={styles.headerStatValue}>{detail.groupName || 'unknown'}</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div style={{ padding: '1rem' }}>
|
|
||||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Route</div>
|
|
||||||
<MonoText>{detail.routeId}</MonoText>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div style={{ padding: '1rem' }}>
|
|
||||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Application</div>
|
|
||||||
<Badge label={detail.groupName || 'unknown'} color="auto" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detail.errorMessage && (
|
{detail.errorMessage && (
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<InfoCallout variant="error">
|
<InfoCallout variant="error">
|
||||||
{detail.errorMessage}
|
{detail.errorMessage}
|
||||||
</InfoCallout>
|
</InfoCallout>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h3 style={{ marginBottom: '0.75rem' }}>Processor Timeline</h3>
|
<div className={styles.timelineSection}>
|
||||||
|
<div className={styles.timelineHeader}>
|
||||||
|
<span className={styles.timelineTitle}>Processor Timeline</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.timelineBody}>
|
||||||
{processors.length > 0 ? (
|
{processors.length > 0 ? (
|
||||||
<ProcessorTimeline
|
<ProcessorTimeline
|
||||||
processors={processors}
|
processors={processors}
|
||||||
@@ -92,39 +91,49 @@ export default function ExchangeDetail() {
|
|||||||
) : (
|
) : (
|
||||||
<InfoCallout>No processor data available</InfoCallout>
|
<InfoCallout>No processor data available</InfoCallout>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{snapshot && (
|
{snapshot && (
|
||||||
<div style={{ marginTop: '1.5rem' }}>
|
<>
|
||||||
<h3 style={{ marginBottom: '0.75rem' }}>Exchange Snapshot</h3>
|
<div className={styles.sectionLabel}>Exchange Snapshot</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
<div className={styles.detailSplit}>
|
||||||
<Card>
|
<div className={styles.detailPanel}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.panelHeader}>
|
||||||
<h4 style={{ marginBottom: '0.5rem' }}>Input Body</h4>
|
<span className={styles.panelTitle}>Input Body</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.panelBody}>
|
||||||
<CodeBlock content={String(snapshot.inputBody ?? 'null')} />
|
<CodeBlock content={String(snapshot.inputBody ?? 'null')} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
<Card>
|
<div className={styles.detailPanel}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.panelHeader}>
|
||||||
<h4 style={{ marginBottom: '0.5rem' }}>Output Body</h4>
|
<span className={styles.panelTitle}>Output Body</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.panelBody}>
|
||||||
<CodeBlock content={String(snapshot.outputBody ?? 'null')} />
|
<CodeBlock content={String(snapshot.outputBody ?? 'null')} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
|
</div>
|
||||||
<Card>
|
<div className={styles.detailSplit}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.detailPanel}>
|
||||||
<h4 style={{ marginBottom: '0.5rem' }}>Input Headers</h4>
|
<div className={styles.panelHeader}>
|
||||||
|
<span className={styles.panelTitle}>Input Headers</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.panelBody}>
|
||||||
<CodeBlock content={JSON.stringify(snapshot.inputHeaders ?? {}, null, 2)} />
|
<CodeBlock content={JSON.stringify(snapshot.inputHeaders ?? {}, null, 2)} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
<Card>
|
<div className={styles.detailPanel}>
|
||||||
<div style={{ padding: '1rem' }}>
|
<div className={styles.panelHeader}>
|
||||||
<h4 style={{ marginBottom: '0.5rem' }}>Output Headers</h4>
|
<span className={styles.panelTitle}>Output Headers</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.panelBody}>
|
||||||
<CodeBlock content={JSON.stringify(snapshot.outputHeaders ?? {}, null, 2)} />
|
<CodeBlock content={JSON.stringify(snapshot.outputHeaders ?? {}, null, 2)} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
63
ui/src/pages/Routes/RoutesMetrics.module.css
Normal file
63
ui/src/pages/Routes/RoutesMetrics.module.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.statStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableMeta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartTitle {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rateGood { color: var(--success); }
|
||||||
|
.rateWarn { color: var(--warning); }
|
||||||
|
.rateBad { color: var(--error); }
|
||||||
@@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system';
|
|||||||
import { useRouteMetrics } from '../../api/queries/catalog';
|
import { useRouteMetrics } from '../../api/queries/catalog';
|
||||||
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
|
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
|
import styles from './RoutesMetrics.module.css';
|
||||||
|
|
||||||
interface RouteRow {
|
interface RouteRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -68,7 +69,11 @@ export default function RoutesMetrics() {
|
|||||||
{ key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(0)}ms` },
|
{ key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(0)}ms` },
|
||||||
{
|
{
|
||||||
key: 'errorRate', header: 'Error Rate', sortable: true,
|
key: 'errorRate', header: 'Error Rate', sortable: true,
|
||||||
render: (v) => <span style={{ color: (v as number) > 0.05 ? 'var(--error)' : undefined }}>{((v as number) * 100).toFixed(1)}%</span>,
|
render: (v) => {
|
||||||
|
const rate = v as number;
|
||||||
|
const cls = rate > 0.05 ? styles.rateBad : rate > 0.01 ? styles.rateWarn : styles.rateGood;
|
||||||
|
return <span className={cls}>{(rate * 100).toFixed(1)}%</span>;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sparkline', header: 'Trend', width: '80px',
|
key: 'sparkline', header: 'Trend', width: '80px',
|
||||||
@@ -78,27 +83,45 @@ export default function RoutesMetrics() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div className={styles.statStrip}>
|
||||||
<StatCard label="Total Throughput" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
<StatCard label="Total Throughput" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
||||||
<StatCard label="Error Rate" value={stats?.totalCount ? `${(((stats.failedCount ?? 0) / stats.totalCount) * 100).toFixed(1)}%` : '0%'} accent="error" />
|
<StatCard label="Error Rate" value={stats?.totalCount ? `${(((stats.failedCount ?? 0) / stats.totalCount) * 100).toFixed(1)}%` : '0%'} accent="error" />
|
||||||
<StatCard label="P99 Latency" value={`${stats?.p99LatencyMs ?? 0}ms`} accent="warning" />
|
<StatCard label="P99 Latency" value={`${stats?.p99LatencyMs ?? 0}ms`} accent="warning" />
|
||||||
<StatCard label="Success Rate" value={stats?.totalCount ? `${(((stats.totalCount - (stats.failedCount ?? 0)) / stats.totalCount) * 100).toFixed(1)}%` : '100%'} accent="success" />
|
<StatCard label="Success Rate" value={stats?.totalCount ? `${(((stats.totalCount - (stats.failedCount ?? 0)) / stats.totalCount) * 100).toFixed(1)}%` : '100%'} accent="success" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.tableSection}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span className={styles.tableTitle}>Route Metrics</span>
|
||||||
|
<span className={styles.tableMeta}>{rows.length} routes</span>
|
||||||
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
sortable
|
sortable
|
||||||
pageSize={20}
|
pageSize={20}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{chartData.length > 0 && (
|
{chartData.length > 0 && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1.5rem' }}>
|
<div className={styles.chartGrid}>
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<div className={styles.chartTitle}>Throughput</div>
|
||||||
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }]} height={200} />
|
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ 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: any, i: number) => ({ x: i, y: d.latency })) }]} height={200} />
|
<LineChart series={[{ label: 'Latency', data: chartData.map((d: any, i: number) => ({ x: i, y: d.latency })) }]} height={200} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<div className={styles.chartTitle}>Errors</div>
|
||||||
<BarChart series={[{ label: 'Errors', data: chartData.map((d: any) => ({ x: d.time as string, y: d.errors })) }]} height={200} />
|
<BarChart series={[{ label: 'Errors', data: chartData.map((d: any) => ({ x: d.time as string, 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: any, i: number) => ({ x: i, y: d.successRate })) }]} height={200} />
|
<AreaChart series={[{ label: 'Success Rate', data: chartData.map((d: any, i: number) => ({ x: i, y: d.successRate })) }]} height={200} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user