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;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
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 { useParams, useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText,
|
StatCard, StatusDot, Badge, MonoText,
|
||||||
GroupCard, EventFeed, Breadcrumb, Alert,
|
GroupCard, EventFeed, Alert,
|
||||||
DetailPanel, ProgressBar, LineChart,
|
DetailPanel, ProgressBar, LineChart,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import styles from './AgentHealth.module.css';
|
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 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 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(() =>
|
const feedEvents = useMemo(() =>
|
||||||
(events || []).map((e: any) => ({
|
(events || []).map((e: any) => ({
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
@@ -225,22 +207,25 @@ export default function AgentHealth() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.statStrip}>
|
<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="Applications" value={uniqueApps} />
|
||||||
<StatCard label="Active Routes" value={activeRoutes} />
|
<StatCard label="Active Routes" value={activeRoutes} />
|
||||||
<StatCard label="Total TPS" value={totalTps.toFixed(1)} />
|
<StatCard label="Total TPS" value={totalTps.toFixed(1)} detail="msg/s" />
|
||||||
<StatCard label="Dead" value={deadCount} accent={deadCount > 0 ? 'error' : undefined} />
|
<StatCard label="Dead" value={deadCount} accent={deadCount > 0 ? 'error' : undefined} detail={deadCount > 0 ? 'requires attention' : undefined} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.scopeTrail}>
|
<div className={styles.scopeTrail}>
|
||||||
<Breadcrumb items={scopeItems} />
|
<span className={styles.scopeLabel}>{liveCount}/{(agents || []).length} live</span>
|
||||||
{!appId && <Badge label={`${liveCount} live`} variant="outlined" />}
|
|
||||||
{appId && (
|
|
||||||
<Badge
|
|
||||||
label={groupHealth}
|
|
||||||
color={groupHealth === 'live' ? 'success' : groupHealth === 'stale' ? 'warning' : 'error'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.groupGrid}>
|
<div className={styles.groupGrid}>
|
||||||
@@ -349,29 +334,26 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
{feedEvents.length > 0 && (
|
{feedEvents.length > 0 && (
|
||||||
<div className={styles.eventCard}>
|
<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} />
|
<EventFeed events={feedEvents} maxItems={100} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedAgent && (
|
{selectedAgent && (
|
||||||
<DetailPanel
|
<DetailPanel
|
||||||
open={!!selectedAgent}
|
key={selectedAgent.id}
|
||||||
|
open={true}
|
||||||
title={selectedAgent.name ?? selectedAgent.id}
|
title={selectedAgent.name ?? selectedAgent.id}
|
||||||
onClose={() => setSelectedAgent(null)}
|
onClose={() => setSelectedAgent(null)}
|
||||||
tabs={[
|
className={styles.detailPanelOverride}
|
||||||
{
|
>
|
||||||
label: 'Overview',
|
<AgentOverviewContent agent={selectedAgent} />
|
||||||
value: 'overview',
|
<div className={styles.panelDivider} />
|
||||||
content: <AgentOverviewContent agent={selectedAgent} />,
|
<AgentPerformanceContent agent={selectedAgent} />
|
||||||
},
|
</DetailPanel>
|
||||||
{
|
|
||||||
label: 'Performance',
|
|
||||||
value: 'performance',
|
|
||||||
content: <AgentPerformanceContent agent={selectedAgent} />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user