Files
design-system/src/pages/AgentHealth/AgentHealth.tsx

263 lines
11 KiB
TypeScript
Raw Normal View History

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, exchangeCount: 1433 },
{ id: 'payment-svc', name: 'payment-svc', agentCount: 1, health: 'live' as const, exchangeCount: 912 },
{ id: 'shipment-tracker', name: 'shipment-tracker', agentCount: 2, health: 'live' as const, exchangeCount: 471 },
{ id: 'notification-hub', name: 'notification-hub', agentCount: 1, health: 'stale' as const, exchangeCount: 128 },
]
const SIDEBAR_ROUTES = routes.slice(0, 3).map((r) => ({
id: r.id,
name: r.name,
exchangeCount: r.exchangeCount,
}))
// ─── 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>
)
}