fix: align AgentHealth page with mock design
- DetailPanel: switch from tabs to flat children layout (fixes stale tab state bug), add position:fixed override, key on agent id - Stat strip: colored status breakdown (live/stale/dead), msg/s detail on TPS, "requires attention" on dead count - Scope trail: simplified to "X/Y live" label - Event card header: rename "Event Log" to "Timeline" with count badge - Remove unused Breadcrumb, scopeItems, groupHealth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,3 +244,36 @@
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Status breakdown in stat card */
|
||||
.statusBreakdown {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.statusLive { color: var(--success); }
|
||||
.statusStale { color: var(--warning); }
|
||||
.statusDead { color: var(--error); }
|
||||
|
||||
/* Scope trail */
|
||||
.scopeLabel {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* DetailPanel override */
|
||||
.detailPanelOverride {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
z-index: 100;
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.panelDivider {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import {
|
||||
StatCard, StatusDot, Badge, MonoText,
|
||||
GroupCard, EventFeed, Breadcrumb, Alert,
|
||||
GroupCard, EventFeed, Alert,
|
||||
DetailPanel, ProgressBar, LineChart,
|
||||
} from '@cameleer/design-system';
|
||||
import styles from './AgentHealth.module.css';
|
||||
@@ -189,24 +189,6 @@ export default function AgentHealth() {
|
||||
const activeRoutes = (agents || []).filter((a: any) => a.status === 'LIVE').reduce((sum: number, a: any) => sum + (a.activeRoutes || 0), 0);
|
||||
const totalTps = (agents || []).filter((a: any) => a.status === 'LIVE').reduce((sum: number, a: any) => sum + (a.tps || 0), 0);
|
||||
|
||||
const groupHealth: 'live' | 'stale' | 'dead' = useMemo(() => {
|
||||
if (!appId) return 'live';
|
||||
const groupAgents = agentsByApp[appId] || [];
|
||||
if (groupAgents.some((a: any) => a.status === 'DEAD')) return 'dead';
|
||||
if (groupAgents.some((a: any) => a.status === 'STALE')) return 'stale';
|
||||
return 'live';
|
||||
}, [appId, agentsByApp]);
|
||||
|
||||
const scopeItems = useMemo(() => {
|
||||
const items: { label: string; href?: string }[] = [
|
||||
{ label: 'Agent Health', href: '/agents' },
|
||||
];
|
||||
if (appId) {
|
||||
items.push({ label: appId });
|
||||
}
|
||||
return items;
|
||||
}, [appId]);
|
||||
|
||||
const feedEvents = useMemo(() =>
|
||||
(events || []).map((e: any) => ({
|
||||
id: String(e.id),
|
||||
@@ -225,22 +207,25 @@ export default function AgentHealth() {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.statStrip}>
|
||||
<StatCard label="Total Agents" value={(agents || []).length} detail={`${liveCount} live / ${staleCount} stale / ${deadCount} dead`} />
|
||||
<StatCard
|
||||
label="Total Agents"
|
||||
value={(agents || []).length}
|
||||
detail={
|
||||
<span className={styles.statusBreakdown}>
|
||||
<span className={styles.statusLive}>{liveCount} live</span>
|
||||
<span className={styles.statusStale}>{staleCount} stale</span>
|
||||
<span className={styles.statusDead}>{deadCount} dead</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StatCard label="Applications" value={uniqueApps} />
|
||||
<StatCard label="Active Routes" value={activeRoutes} />
|
||||
<StatCard label="Total TPS" value={totalTps.toFixed(1)} />
|
||||
<StatCard label="Dead" value={deadCount} accent={deadCount > 0 ? 'error' : undefined} />
|
||||
<StatCard label="Total TPS" value={totalTps.toFixed(1)} detail="msg/s" />
|
||||
<StatCard label="Dead" value={deadCount} accent={deadCount > 0 ? 'error' : undefined} detail={deadCount > 0 ? 'requires attention' : undefined} />
|
||||
</div>
|
||||
|
||||
<div className={styles.scopeTrail}>
|
||||
<Breadcrumb items={scopeItems} />
|
||||
{!appId && <Badge label={`${liveCount} live`} variant="outlined" />}
|
||||
{appId && (
|
||||
<Badge
|
||||
label={groupHealth}
|
||||
color={groupHealth === 'live' ? 'success' : groupHealth === 'stale' ? 'warning' : 'error'}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.scopeLabel}>{liveCount}/{(agents || []).length} live</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.groupGrid}>
|
||||
@@ -349,29 +334,26 @@ export default function AgentHealth() {
|
||||
|
||||
{feedEvents.length > 0 && (
|
||||
<div className={styles.eventCard}>
|
||||
<div className={styles.eventCardHeader}>Event Log</div>
|
||||
<div className={styles.eventCardHeader}>
|
||||
<span>Timeline</span>
|
||||
<Badge label={`${feedEvents.length} events`} variant="outlined" />
|
||||
</div>
|
||||
<EventFeed events={feedEvents} maxItems={100} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAgent && (
|
||||
<DetailPanel
|
||||
open={!!selectedAgent}
|
||||
key={selectedAgent.id}
|
||||
open={true}
|
||||
title={selectedAgent.name ?? selectedAgent.id}
|
||||
onClose={() => setSelectedAgent(null)}
|
||||
tabs={[
|
||||
{
|
||||
label: 'Overview',
|
||||
value: 'overview',
|
||||
content: <AgentOverviewContent agent={selectedAgent} />,
|
||||
},
|
||||
{
|
||||
label: 'Performance',
|
||||
value: 'performance',
|
||||
content: <AgentPerformanceContent agent={selectedAgent} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
className={styles.detailPanelOverride}
|
||||
>
|
||||
<AgentOverviewContent agent={selectedAgent} />
|
||||
<div className={styles.panelDivider} />
|
||||
<AgentPerformanceContent agent={selectedAgent} />
|
||||
</DetailPanel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user