refactor: AgentHealth slide-in detail panel and richer stat cards
All checks were successful
Build & Publish / publish (push) Successful in 43s
All checks were successful
Build & Publish / publish (push) Successful in 43s
- Instance detail now opens in a DetailPanel (slide-in from right) with Overview and Performance tabs instead of navigating away - Stat strip matches mock design: 5 cards with colored StatusDot breakdowns, labeled states (live/stale/dead, healthy/degraded/critical) - Active Routes shows colored ratio (green/yellow/red) based on state - Groups renamed to Applications - StatCard value/detail props now accept ReactNode for rich content Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import styles from './StatCard.module.css'
|
import styles from './StatCard.module.css'
|
||||||
import { Sparkline } from '../Sparkline/Sparkline'
|
import { Sparkline } from '../Sparkline/Sparkline'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
label: string
|
label: string
|
||||||
value: string | number
|
value: ReactNode
|
||||||
detail?: string
|
detail?: ReactNode
|
||||||
trend?: 'up' | 'down' | 'neutral'
|
trend?: 'up' | 'down' | 'neutral'
|
||||||
trendValue?: string
|
trendValue?: string
|
||||||
accent?: 'amber' | 'success' | 'warning' | 'error' | 'running'
|
accent?: 'amber' | 'success' | 'warning' | 'error' | 'running'
|
||||||
|
|||||||
@@ -10,11 +10,27 @@
|
|||||||
/* Stat strip */
|
/* Stat strip */
|
||||||
.statStrip {
|
.statStrip {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stat breakdown with colored dots */
|
||||||
|
.breakdown {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bpLive { color: var(--success); display: inline-flex; align-items: center; gap: 3px; }
|
||||||
|
.bpStale { color: var(--warning); display: inline-flex; align-items: center; gap: 3px; }
|
||||||
|
.bpDead { color: var(--error); display: inline-flex; align-items: center; gap: 3px; }
|
||||||
|
|
||||||
|
.routesSuccess { color: var(--success); }
|
||||||
|
.routesWarning { color: var(--warning); }
|
||||||
|
.routesError { color: var(--error); }
|
||||||
|
|
||||||
/* Scope breadcrumb trail */
|
/* Scope breadcrumb trail */
|
||||||
.scopeTrail {
|
.scopeTrail {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -178,11 +194,6 @@
|
|||||||
box-shadow: inset 3px 0 0 var(--amber);
|
box-shadow: inset 3px 0 0 var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart expansion row */
|
|
||||||
.chartRow td {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Instance fields */
|
/* Instance fields */
|
||||||
.instanceName {
|
.instanceName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -211,17 +222,35 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Instance expanded charts */
|
/* Detail panel content */
|
||||||
.instanceCharts {
|
.detailContent {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
}
|
||||||
background: var(--bg-raised);
|
|
||||||
border-top: 1px solid var(--border-subtle);
|
.detailRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
padding: 4px 0;
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailProgress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.chartPanel {
|
.chartPanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
import { useParams, Link } from 'react-router-dom'
|
||||||
import styles from './AgentHealth.module.css'
|
import styles from './AgentHealth.module.css'
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
@@ -11,12 +11,14 @@ import { TopBar } from '../../design-system/layout/TopBar/TopBar'
|
|||||||
import { GroupCard } from '../../design-system/composites/GroupCard/GroupCard'
|
import { GroupCard } from '../../design-system/composites/GroupCard/GroupCard'
|
||||||
import { LineChart } from '../../design-system/composites/LineChart/LineChart'
|
import { LineChart } from '../../design-system/composites/LineChart/LineChart'
|
||||||
import { EventFeed } from '../../design-system/composites/EventFeed/EventFeed'
|
import { EventFeed } from '../../design-system/composites/EventFeed/EventFeed'
|
||||||
|
import { DetailPanel } from '../../design-system/composites/DetailPanel/DetailPanel'
|
||||||
|
|
||||||
// Primitives
|
// Primitives
|
||||||
import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
||||||
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||||
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||||
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
||||||
|
import { ProgressBar } from '../../design-system/primitives/ProgressBar/ProgressBar'
|
||||||
|
|
||||||
// Global filters
|
// Global filters
|
||||||
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
||||||
@@ -31,13 +33,11 @@ import { agentEvents } from '../../mocks/agentEvents'
|
|||||||
type Scope =
|
type Scope =
|
||||||
| { level: 'all' }
|
| { level: 'all' }
|
||||||
| { level: 'app'; appId: string }
|
| { level: 'app'; appId: string }
|
||||||
| { level: 'instance'; appId: string; instanceId: string }
|
|
||||||
|
|
||||||
function useScope(): Scope {
|
function useScope(): Scope {
|
||||||
const { '*': rest } = useParams()
|
const { '*': rest } = useParams()
|
||||||
const segments = rest?.split('/').filter(Boolean) ?? []
|
const segments = rest?.split('/').filter(Boolean) ?? []
|
||||||
if (segments.length >= 2) return { level: 'instance', appId: segments[0], instanceId: segments[1] }
|
if (segments.length >= 1) return { level: 'app', appId: segments[0] }
|
||||||
if (segments.length === 1) return { level: 'app', appId: segments[0] }
|
|
||||||
return { level: 'all' }
|
return { level: 'all' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +106,8 @@ function buildBreadcrumb(scope: Scope) {
|
|||||||
{ label: 'Applications', href: '/apps' },
|
{ label: 'Applications', href: '/apps' },
|
||||||
{ label: 'Agents', href: '/agents' },
|
{ label: 'Agents', href: '/agents' },
|
||||||
]
|
]
|
||||||
if (scope.level === 'app' || scope.level === 'instance') {
|
if (scope.level === 'app') {
|
||||||
crumbs.push({ label: scope.appId, href: `/agents/${scope.appId}` })
|
crumbs.push({ label: scope.appId })
|
||||||
}
|
|
||||||
if (scope.level === 'instance') {
|
|
||||||
crumbs.push({ label: scope.instanceId })
|
|
||||||
}
|
}
|
||||||
return crumbs
|
return crumbs
|
||||||
}
|
}
|
||||||
@@ -119,14 +116,14 @@ function buildBreadcrumb(scope: Scope) {
|
|||||||
|
|
||||||
export function AgentHealth() {
|
export function AgentHealth() {
|
||||||
const scope = useScope()
|
const scope = useScope()
|
||||||
const navigate = useNavigate()
|
|
||||||
const { isInTimeRange } = useGlobalFilters()
|
const { isInTimeRange } = useGlobalFilters()
|
||||||
|
const [selectedInstance, setSelectedInstance] = useState<AgentHealthData | null>(null)
|
||||||
|
const [panelOpen, setPanelOpen] = useState(false)
|
||||||
|
|
||||||
// Filter agents by scope
|
// Filter agents by scope
|
||||||
const filteredAgents = useMemo(() => {
|
const filteredAgents = useMemo(() => {
|
||||||
if (scope.level === 'all') return agents
|
if (scope.level === 'all') return agents
|
||||||
if (scope.level === 'app') return agents.filter((a) => a.appId === scope.appId)
|
return agents.filter((a) => a.appId === scope.appId)
|
||||||
return agents.filter((a) => a.appId === scope.appId && a.id === scope.instanceId)
|
|
||||||
}, [scope])
|
}, [scope])
|
||||||
|
|
||||||
const groups = useMemo(() => groupByApp(filteredAgents), [filteredAgents])
|
const groups = useMemo(() => groupByApp(filteredAgents), [filteredAgents])
|
||||||
@@ -138,18 +135,132 @@ export function AgentHealth() {
|
|||||||
const deadCount = filteredAgents.filter((a) => a.status === 'dead').length
|
const deadCount = filteredAgents.filter((a) => a.status === 'dead').length
|
||||||
const totalTps = filteredAgents.reduce((s, a) => s + a.tps, 0)
|
const totalTps = filteredAgents.reduce((s, a) => s + a.tps, 0)
|
||||||
const totalActiveRoutes = filteredAgents.reduce((s, a) => s + a.activeRoutes, 0)
|
const totalActiveRoutes = filteredAgents.reduce((s, a) => s + a.activeRoutes, 0)
|
||||||
|
const totalRoutes = filteredAgents.reduce((s, a) => s + a.totalRoutes, 0)
|
||||||
|
|
||||||
// Filter events by global time range
|
// Filter events by global time range
|
||||||
const filteredEvents = agentEvents.filter((e) => isInTimeRange(e.timestamp))
|
const filteredEvents = agentEvents.filter((e) => isInTimeRange(e.timestamp))
|
||||||
|
|
||||||
// Single instance for expanded charts
|
// Build trend data for selected instance
|
||||||
const singleInstance = scope.level === 'instance' ? filteredAgents[0] : null
|
const trendData = selectedInstance ? buildTrendData(selectedInstance) : null
|
||||||
const trendData = singleInstance ? buildTrendData(singleInstance) : null
|
|
||||||
|
function handleInstanceClick(inst: AgentHealthData) {
|
||||||
|
setSelectedInstance(inst)
|
||||||
|
setPanelOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail panel tabs
|
||||||
|
const detailTabs = selectedInstance
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Overview',
|
||||||
|
value: 'overview',
|
||||||
|
content: (
|
||||||
|
<div className={styles.detailContent}>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Status</span>
|
||||||
|
<Badge
|
||||||
|
label={selectedInstance.status.toUpperCase()}
|
||||||
|
color={selectedInstance.status === 'live' ? 'success' : selectedInstance.status === 'stale' ? 'warning' : 'error'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Application</span>
|
||||||
|
<MonoText size="xs">{selectedInstance.appId}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Version</span>
|
||||||
|
<MonoText size="xs">{selectedInstance.version}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Uptime</span>
|
||||||
|
<MonoText size="xs">{selectedInstance.uptime}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Last Seen</span>
|
||||||
|
<MonoText size="xs">{selectedInstance.lastSeen}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Throughput</span>
|
||||||
|
<MonoText size="xs">{selectedInstance.tps.toFixed(1)}/s</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Errors</span>
|
||||||
|
<MonoText size="xs" className={selectedInstance.errorRate ? styles.instanceError : undefined}>
|
||||||
|
{selectedInstance.errorRate ?? '0 err/h'}
|
||||||
|
</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Routes</span>
|
||||||
|
<span>{selectedInstance.activeRoutes}/{selectedInstance.totalRoutes} active</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Memory</span>
|
||||||
|
<div className={styles.detailProgress}>
|
||||||
|
<ProgressBar
|
||||||
|
value={selectedInstance.memoryUsagePct}
|
||||||
|
variant={selectedInstance.memoryUsagePct > 85 ? 'error' : selectedInstance.memoryUsagePct > 70 ? 'warning' : 'success'}
|
||||||
|
/>
|
||||||
|
<MonoText size="xs">{selectedInstance.memoryUsagePct}%</MonoText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>CPU</span>
|
||||||
|
<div className={styles.detailProgress}>
|
||||||
|
<ProgressBar
|
||||||
|
value={selectedInstance.cpuUsagePct}
|
||||||
|
variant={selectedInstance.cpuUsagePct > 85 ? 'error' : selectedInstance.cpuUsagePct > 70 ? 'warning' : 'success'}
|
||||||
|
/>
|
||||||
|
<MonoText size="xs">{selectedInstance.cpuUsagePct}%</MonoText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Performance',
|
||||||
|
value: 'performance',
|
||||||
|
content: trendData ? (
|
||||||
|
<div className={styles.detailContent}>
|
||||||
|
<div className={styles.chartPanel}>
|
||||||
|
<div className={styles.chartTitle}>Throughput (msg/s)</div>
|
||||||
|
<LineChart
|
||||||
|
series={[{ label: 'tps', data: trendData.throughput }]}
|
||||||
|
height={160}
|
||||||
|
width={360}
|
||||||
|
yLabel="msg/s"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.chartPanel}>
|
||||||
|
<div className={styles.chartTitle}>Error Rate (err/h)</div>
|
||||||
|
<LineChart
|
||||||
|
series={[{ label: 'errors', data: trendData.errorRate, color: 'var(--error)' }]}
|
||||||
|
height={160}
|
||||||
|
width={360}
|
||||||
|
yLabel="err/h"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
const isFullWidth = scope.level !== 'all'
|
const isFullWidth = scope.level !== 'all'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell sidebar={<Sidebar apps={SIDEBAR_APPS} />}>
|
<AppShell
|
||||||
|
sidebar={<Sidebar apps={SIDEBAR_APPS} />}
|
||||||
|
detail={
|
||||||
|
selectedInstance ? (
|
||||||
|
<DetailPanel
|
||||||
|
open={panelOpen}
|
||||||
|
onClose={() => setPanelOpen(false)}
|
||||||
|
title={selectedInstance.name}
|
||||||
|
tabs={detailTabs}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<TopBar
|
<TopBar
|
||||||
breadcrumb={buildBreadcrumb(scope)}
|
breadcrumb={buildBreadcrumb(scope)}
|
||||||
environment="PRODUCTION"
|
environment="PRODUCTION"
|
||||||
@@ -159,35 +270,65 @@ export function AgentHealth() {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* Stat strip */}
|
{/* Stat strip */}
|
||||||
<div className={styles.statStrip}>
|
<div className={styles.statStrip}>
|
||||||
<StatCard label="Total Instances" value={String(totalInstances)} />
|
<StatCard
|
||||||
<StatCard label="Live" value={String(liveCount)} accent="success" />
|
label="Total Agents"
|
||||||
<StatCard label="Stale" value={String(staleCount)} accent={staleCount > 0 ? 'warning' : undefined} />
|
value={String(totalInstances)}
|
||||||
<StatCard label="Dead" value={String(deadCount)} accent={deadCount > 0 ? 'error' : undefined} />
|
accent={deadCount > 0 ? 'warning' : 'amber'}
|
||||||
<StatCard label="Total TPS" value={`${totalTps.toFixed(1)}/s`} />
|
detail={
|
||||||
<StatCard label="Active Routes" value={String(totalActiveRoutes)} />
|
<span className={styles.breakdown}>
|
||||||
|
<span className={styles.bpLive}><StatusDot variant="live" /> {liveCount} live</span>
|
||||||
|
<span className={styles.bpStale}><StatusDot variant="stale" /> {staleCount} stale</span>
|
||||||
|
<span className={styles.bpDead}><StatusDot variant="dead" /> {deadCount} dead</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Applications"
|
||||||
|
value={String(groups.length)}
|
||||||
|
accent="running"
|
||||||
|
detail={
|
||||||
|
<span className={styles.breakdown}>
|
||||||
|
<span className={styles.bpLive}><StatusDot variant="live" /> {groups.filter((g) => g.deadCount === 0 && g.staleCount === 0).length} healthy</span>
|
||||||
|
<span className={styles.bpStale}><StatusDot variant="stale" /> {groups.filter((g) => g.staleCount > 0 && g.deadCount === 0).length} degraded</span>
|
||||||
|
<span className={styles.bpDead}><StatusDot variant="dead" /> {groups.filter((g) => g.deadCount > 0).length} critical</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Active Routes"
|
||||||
|
value={<span className={styles[totalActiveRoutes === 0 ? 'routesError' : totalActiveRoutes < totalRoutes ? 'routesWarning' : 'routesSuccess']}>{totalActiveRoutes}/{totalRoutes}</span>}
|
||||||
|
accent={totalActiveRoutes === 0 ? 'error' : totalActiveRoutes < totalRoutes ? 'warning' : 'success'}
|
||||||
|
detail={totalActiveRoutes < totalRoutes ? `${totalRoutes - totalActiveRoutes} suspended` : 'all routes active'}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Total TPS"
|
||||||
|
value={totalTps.toFixed(1)}
|
||||||
|
accent="amber"
|
||||||
|
detail="msg/s"
|
||||||
|
trend="up"
|
||||||
|
trendValue="4.2%"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Dead"
|
||||||
|
value={String(deadCount)}
|
||||||
|
accent={deadCount > 0 ? 'error' : 'success'}
|
||||||
|
detail={deadCount > 0 ? 'requires attention' : 'all healthy'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope breadcrumb trail */}
|
{/* Scope breadcrumb trail */}
|
||||||
{scope.level !== 'all' && (
|
{scope.level !== 'all' && (
|
||||||
<div className={styles.scopeTrail}>
|
<div className={styles.scopeTrail}>
|
||||||
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
||||||
{scope.level === 'instance' && (
|
|
||||||
<>
|
|
||||||
<span className={styles.scopeSep}>▸</span>
|
|
||||||
<Link to={`/agents/${scope.appId}`} className={styles.scopeLink}>{scope.appId}</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className={styles.scopeSep}>▸</span>
|
<span className={styles.scopeSep}>▸</span>
|
||||||
<span className={styles.scopeCurrent}>
|
<span className={styles.scopeCurrent}>{scope.appId}</span>
|
||||||
{scope.level === 'app' ? scope.appId : scope.instanceId}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section header */}
|
{/* Section header */}
|
||||||
<div className={styles.sectionHeaderRow}>
|
<div className={styles.sectionHeaderRow}>
|
||||||
<span className={styles.sectionTitle}>
|
<span className={styles.sectionTitle}>
|
||||||
{scope.level === 'all' ? 'Agents' : scope.level === 'app' ? scope.appId : scope.instanceId}
|
{scope.level === 'all' ? 'Agents' : scope.appId}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
label={`${liveCount}/${totalInstances} live`}
|
label={`${liveCount}/${totalInstances} live`}
|
||||||
@@ -240,78 +381,48 @@ export function AgentHealth() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{group.instances.map((inst) => (
|
{group.instances.map((inst) => (
|
||||||
<>
|
<tr
|
||||||
<tr
|
key={inst.id}
|
||||||
key={inst.id}
|
className={[
|
||||||
className={[
|
styles.instanceRow,
|
||||||
styles.instanceRow,
|
selectedInstance?.id === inst.id && panelOpen ? styles.instanceRowActive : '',
|
||||||
scope.level === 'instance' && scope.instanceId === inst.id ? styles.instanceRowActive : '',
|
].filter(Boolean).join(' ')}
|
||||||
].filter(Boolean).join(' ')}
|
onClick={() => handleInstanceClick(inst)}
|
||||||
onClick={() => navigate(`/agents/${inst.appId}/${inst.id}`)}
|
>
|
||||||
>
|
<td className={styles.tdStatus}>
|
||||||
<td className={styles.tdStatus}>
|
<StatusDot variant={inst.status === 'live' ? 'live' : inst.status === 'stale' ? 'stale' : 'dead'} />
|
||||||
<StatusDot variant={inst.status === 'live' ? 'live' : inst.status === 'stale' ? 'stale' : 'dead'} />
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<MonoText size="sm" className={styles.instanceName}>{inst.name}</MonoText>
|
||||||
<MonoText size="sm" className={styles.instanceName}>{inst.name}</MonoText>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<Badge
|
||||||
<Badge
|
label={inst.status.toUpperCase()}
|
||||||
label={inst.status.toUpperCase()}
|
color={inst.status === 'live' ? 'success' : inst.status === 'stale' ? 'warning' : 'error'}
|
||||||
color={inst.status === 'live' ? 'success' : inst.status === 'stale' ? 'warning' : 'error'}
|
variant="filled"
|
||||||
variant="filled"
|
/>
|
||||||
/>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<MonoText size="xs" className={styles.instanceMeta}>{inst.uptime}</MonoText>
|
||||||
<MonoText size="xs" className={styles.instanceMeta}>{inst.uptime}</MonoText>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<MonoText size="xs" className={styles.instanceMeta}>{inst.tps.toFixed(1)}/s</MonoText>
|
||||||
<MonoText size="xs" className={styles.instanceMeta}>{inst.tps.toFixed(1)}/s</MonoText>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<MonoText size="xs" className={inst.errorRate ? styles.instanceError : styles.instanceMeta}>
|
||||||
<MonoText size="xs" className={inst.errorRate ? styles.instanceError : styles.instanceMeta}>
|
{inst.errorRate ?? '0 err/h'}
|
||||||
{inst.errorRate ?? '0 err/h'}
|
</MonoText>
|
||||||
</MonoText>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<MonoText size="xs" className={
|
||||||
<MonoText size="xs" className={
|
inst.status === 'dead' ? styles.instanceHeartbeatDead :
|
||||||
inst.status === 'dead' ? styles.instanceHeartbeatDead :
|
inst.status === 'stale' ? styles.instanceHeartbeatStale :
|
||||||
inst.status === 'stale' ? styles.instanceHeartbeatStale :
|
styles.instanceMeta
|
||||||
styles.instanceMeta
|
}>
|
||||||
}>
|
{inst.lastSeen}
|
||||||
{inst.lastSeen}
|
</MonoText>
|
||||||
</MonoText>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Expanded charts for single instance */}
|
|
||||||
{singleInstance?.id === inst.id && trendData && (
|
|
||||||
<tr key={`${inst.id}-charts`} className={styles.chartRow}>
|
|
||||||
<td colSpan={7}>
|
|
||||||
<div className={styles.instanceCharts}>
|
|
||||||
<div className={styles.chartPanel}>
|
|
||||||
<div className={styles.chartTitle}>Throughput (msg/s)</div>
|
|
||||||
<LineChart
|
|
||||||
series={[{ label: 'tps', data: trendData.throughput }]}
|
|
||||||
height={160}
|
|
||||||
width={480}
|
|
||||||
yLabel="msg/s"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.chartPanel}>
|
|
||||||
<div className={styles.chartTitle}>Error Rate (err/h)</div>
|
|
||||||
<LineChart
|
|
||||||
series={[{ label: 'errors', data: trendData.errorRate, color: 'var(--error)' }]}
|
|
||||||
height={160}
|
|
||||||
width={480}
|
|
||||||
yLabel="err/h"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user