feat: ExchangeDetail and AgentHealth pages
ExchangeDetail (/exchanges/:id): exchange header card with ID/route/ status/duration, ProcessorTimeline for the specific exchange, step-by- step exchange inspector using Collapsible+CodeBlock for headers/body at each processor step, and error details block for failed exchanges. AgentHealth (/agents): 6-card system overview strip, 2-column grid of agent cards (StatusDot, name, version, tps, uptime, last-seen, CPU/mem usage, active routes), expandable per-agent LineCharts for throughput and error rate trends. Both pages use AppShell + shared Sidebar layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
262
src/pages/AgentHealth/AgentHealth.tsx
Normal file
262
src/pages/AgentHealth/AgentHealth.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styles from './AgentHealth.module.css'
|
||||
|
||||
// Layout
|
||||
import { AppShell } from '../../design-system/layout/AppShell/AppShell'
|
||||
import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar'
|
||||
import { TopBar } from '../../design-system/layout/TopBar/TopBar'
|
||||
|
||||
// Composites
|
||||
import { LineChart } from '../../design-system/composites/LineChart/LineChart'
|
||||
|
||||
// Primitives
|
||||
import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
||||
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||
import { Card } from '../../design-system/primitives/Card/Card'
|
||||
|
||||
// Mock data
|
||||
import { agents } from '../../mocks/agents'
|
||||
import { routes } from '../../mocks/routes'
|
||||
|
||||
// ─── Sidebar data (shared) ────────────────────────────────────────────────────
|
||||
const APPS = [
|
||||
{ id: 'order-service', name: 'order-service', agentCount: 2, health: 'live' as const, execCount: 1433 },
|
||||
{ id: 'payment-svc', name: 'payment-svc', agentCount: 1, health: 'live' as const, execCount: 912 },
|
||||
{ id: 'shipment-tracker', name: 'shipment-tracker', agentCount: 2, health: 'live' as const, execCount: 471 },
|
||||
{ id: 'notification-hub', name: 'notification-hub', agentCount: 1, health: 'stale' as const, execCount: 128 },
|
||||
]
|
||||
|
||||
const SIDEBAR_ROUTES = routes.slice(0, 3).map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
execCount: r.execCount,
|
||||
}))
|
||||
|
||||
// ─── Build trend data for each agent ─────────────────────────────────────────
|
||||
function buildAgentTrendSeries(agentId: string) {
|
||||
const baseValues: Record<string, { throughput: number; errorRate: number }> = {
|
||||
'prod-1': { throughput: 14.2, errorRate: 0.2 },
|
||||
'prod-2': { throughput: 11.8, errorRate: 3.1 },
|
||||
'prod-3': { throughput: 12.1, errorRate: 0.5 },
|
||||
'prod-4': { throughput: 9.1, errorRate: 0.3 },
|
||||
}
|
||||
const base = baseValues[agentId] ?? { throughput: 10, errorRate: 1 }
|
||||
|
||||
const now = new Date('2026-03-18T09:15:00')
|
||||
const points = 20
|
||||
const intervalMs = (3 * 60 * 60 * 1000) / points // 3 hours
|
||||
|
||||
const throughputData = Array.from({ length: points }, (_, i) => ({
|
||||
x: new Date(now.getTime() - (points - i) * intervalMs),
|
||||
y: Math.max(0, base.throughput + (Math.random() - 0.5) * 4),
|
||||
}))
|
||||
|
||||
const errorRateData = Array.from({ length: points }, (_, i) => ({
|
||||
x: new Date(now.getTime() - (points - i) * intervalMs),
|
||||
y: Math.max(0, base.errorRate + (Math.random() - 0.5) * 2),
|
||||
}))
|
||||
|
||||
return { throughputData, errorRateData }
|
||||
}
|
||||
|
||||
// ─── Summary stats ────────────────────────────────────────────────────────────
|
||||
const liveCount = agents.filter((a) => a.status === 'live').length
|
||||
const totalTps = agents.reduce((sum, a) => sum + parseFloat(a.tps), 0)
|
||||
const totalActiveRoutes = agents.reduce((sum, a) => sum + a.activeRoutes, 0)
|
||||
|
||||
// ─── AgentHealth page ─────────────────────────────────────────────────────────
|
||||
export function AgentHealth() {
|
||||
const navigate = useNavigate()
|
||||
const [activeItem, setActiveItem] = useState('agents')
|
||||
const [expandedAgent, setExpandedAgent] = useState<string | null>(null)
|
||||
|
||||
function handleItemClick(id: string) {
|
||||
setActiveItem(id)
|
||||
const route = routes.find((r) => r.id === id)
|
||||
if (route) navigate(`/routes/${id}`)
|
||||
}
|
||||
|
||||
function toggleAgent(id: string) {
|
||||
setExpandedAgent((prev) => (prev === id ? null : id))
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
sidebar={
|
||||
<Sidebar
|
||||
apps={APPS}
|
||||
routes={SIDEBAR_ROUTES}
|
||||
agents={agents}
|
||||
activeItem={activeItem}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<TopBar
|
||||
breadcrumb={[
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Agents' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className={styles.content}>
|
||||
|
||||
{/* System overview strip */}
|
||||
<div className={styles.overviewStrip}>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Total Agents</div>
|
||||
<div className={styles.overviewValue}>{agents.length}</div>
|
||||
</div>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Live</div>
|
||||
<div className={`${styles.overviewValue} ${styles.valueLive}`}>{liveCount}</div>
|
||||
</div>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Stale</div>
|
||||
<div className={`${styles.overviewValue} ${agents.some(a => a.status === 'stale') ? styles.valueStale : ''}`}>
|
||||
{agents.filter((a) => a.status === 'stale').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Dead</div>
|
||||
<div className={`${styles.overviewValue} ${agents.some(a => a.status === 'dead') ? styles.valueDead : ''}`}>
|
||||
{agents.filter((a) => a.status === 'dead').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Total TPS</div>
|
||||
<div className={styles.overviewValue}>{totalTps.toFixed(1)}/s</div>
|
||||
</div>
|
||||
<div className={styles.overviewCard}>
|
||||
<div className={styles.overviewLabel}>Active Routes</div>
|
||||
<div className={styles.overviewValue}>{totalActiveRoutes}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section header */}
|
||||
<div className={styles.sectionHeaderRow}>
|
||||
<span className={styles.sectionTitle}>Agent Details</span>
|
||||
<span className={styles.sectionMeta}>{liveCount}/{agents.length} live · Click to expand charts</span>
|
||||
</div>
|
||||
|
||||
{/* Agent cards grid */}
|
||||
<div className={styles.agentGrid}>
|
||||
{agents.map((agent) => {
|
||||
const isExpanded = expandedAgent === agent.id
|
||||
const trendData = isExpanded ? buildAgentTrendSeries(agent.id) : null
|
||||
const statusVariant = agent.status === 'live' ? 'live' : agent.status === 'stale' ? 'stale' : 'dead'
|
||||
const cardAccent = agent.status === 'live' ? 'success' : agent.status === 'stale' ? 'warning' : 'error'
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={agent.id}
|
||||
accent={cardAccent as 'success' | 'warning' | 'error'}
|
||||
className={styles.agentCard}
|
||||
>
|
||||
{/* Agent card header */}
|
||||
<div
|
||||
className={styles.agentCardHeader}
|
||||
onClick={() => toggleAgent(agent.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') toggleAgent(agent.id)
|
||||
}}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className={styles.agentCardLeft}>
|
||||
<StatusDot variant={statusVariant} />
|
||||
<div>
|
||||
<div className={styles.agentCardName}>{agent.name}</div>
|
||||
<div className={styles.agentCardService}>{agent.service} {agent.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.agentCardRight}>
|
||||
<Badge
|
||||
label={agent.status.toUpperCase()}
|
||||
color={agent.status === 'live' ? 'success' : agent.status === 'stale' ? 'warning' : 'error'}
|
||||
variant="filled"
|
||||
/>
|
||||
<span className={styles.expandIcon}>{isExpanded ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent metrics row */}
|
||||
<div className={styles.agentMetrics}>
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>TPS</span>
|
||||
<MonoText size="sm" className={styles.metricValue}>{agent.tps}</MonoText>
|
||||
</div>
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>Uptime</span>
|
||||
<MonoText size="sm" className={styles.metricValue}>{agent.uptime}</MonoText>
|
||||
</div>
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>Last Seen</span>
|
||||
<MonoText size="sm" className={styles.metricValue}>{agent.lastSeen}</MonoText>
|
||||
</div>
|
||||
{agent.errorRate && (
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>Error Rate</span>
|
||||
<MonoText size="sm" className={styles.metricValueError}>{agent.errorRate}</MonoText>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>CPU</span>
|
||||
<MonoText size="sm" className={agent.cpuUsagePct > 70 ? styles.metricValueWarn : styles.metricValue}>
|
||||
{agent.cpuUsagePct}%
|
||||
</MonoText>
|
||||
</div>
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>Memory</span>
|
||||
<MonoText size="sm" className={agent.memoryUsagePct > 80 ? styles.metricValueError : agent.memoryUsagePct > 70 ? styles.metricValueWarn : styles.metricValue}>
|
||||
{agent.memoryUsagePct}%
|
||||
</MonoText>
|
||||
</div>
|
||||
<div className={styles.agentMetric}>
|
||||
<span className={styles.metricLabel}>Routes</span>
|
||||
<MonoText size="sm" className={styles.metricValue}>
|
||||
{agent.activeRoutes}/{agent.totalRoutes}
|
||||
</MonoText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded detail: trend charts */}
|
||||
{isExpanded && trendData && (
|
||||
<div className={styles.agentCharts}>
|
||||
<div className={styles.agentChart}>
|
||||
<div className={styles.chartTitle}>Throughput (msg/s)</div>
|
||||
<LineChart
|
||||
series={[{ label: 'tps', data: trendData.throughputData }]}
|
||||
height={140}
|
||||
width={380}
|
||||
yLabel="msg/s"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.agentChart}>
|
||||
<div className={styles.chartTitle}>Error Rate (err/h)</div>
|
||||
<LineChart
|
||||
series={[{ label: 'errors', data: trendData.errorRateData, color: 'var(--error)' }]}
|
||||
height={140}
|
||||
width={380}
|
||||
yLabel="err/h"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user