From 5229e08b2716b428a42708fa11a0700d6fd88227 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:37:58 +0200 Subject: [PATCH] feat: add compact app cards with inline expand to runtime dashboard Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/AgentHealth/AgentHealth.tsx | 221 +++++++++++++++++------ 1 file changed, 170 insertions(+), 51 deletions(-) diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index c7bcfcbe..0087b644 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -80,6 +80,16 @@ function appHealth(group: AppGroup): 'success' | 'warning' | 'error' { return 'success'; } +function latestHeartbeat(group: AppGroup): string | undefined { + let latest: string | undefined; + for (const inst of group.instances) { + if (inst.lastHeartbeat && (!latest || inst.lastHeartbeat > latest)) { + latest = inst.lastHeartbeat; + } + } + return latest; +} + // ── Detail sub-components ──────────────────────────────────────────────────── const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ @@ -96,6 +106,40 @@ const LOG_SOURCE_ITEMS: ButtonGroupItem[] = [ { value: 'container', label: 'Container' }, ]; +function CompactAppCard({ group, onExpand }: { group: AppGroup; onExpand: () => void }) { + const health = appHealth(group); + const heartbeat = latestHeartbeat(group); + const isHealthy = health === 'success'; + + const variantClass = + health === 'success' ? styles.compactCardSuccess + : health === 'warning' ? styles.compactCardWarning + : styles.compactCardError; + + return ( +
{ if (e.key === 'Enter' || e.key === ' ') onExpand(); }} + > +
+ {group.appId} + +
+
+ + {group.liveCount}/{group.instances.length} live + + + {heartbeat ? timeAgo(heartbeat) : '\u2014'} + +
+
+ ); +} + // ── AgentHealth page ───────────────────────────────────────────────────────── export default function AgentHealth() { @@ -550,60 +594,135 @@ export default function AgentHealth() { {/* Group cards grid */} -
- {groups.map((group) => ( - - } - meta={ -
- {group.totalTps.toFixed(1)} msg/s - {group.totalActiveRoutes}/{group.totalRoutes} routes - - - -
- } - footer={ - group.deadCount > 0 ? ( -
- + {viewMode === 'expanded' || isFullWidth ? ( +
+ {groups.map((group) => ( + + } + meta={ +
+ {group.totalTps.toFixed(1)} msg/s + {group.totalActiveRoutes}/{group.totalRoutes} routes - Single point of failure —{' '} - {group.deadCount === group.instances.length - ? 'no redundancy' - : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`} +
- ) : undefined - } - > - - columns={instanceColumns as Column[]} - data={group.instances.map(i => ({ ...i, id: i.instanceId }))} - onRowClick={handleInstanceClick} - pageSize={50} - flush - /> -
- ))} -
+ } + footer={ + group.deadCount > 0 ? ( +
+ + + Single point of failure —{' '} + {group.deadCount === group.instances.length + ? 'no redundancy' + : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`} + +
+ ) : undefined + } + > + + columns={instanceColumns as Column[]} + data={group.instances.map(i => ({ ...i, id: i.instanceId }))} + onRowClick={handleInstanceClick} + pageSize={50} + flush + /> + + ))} +
+ ) : ( +
+ {groups.map((group) => + expandedApps.has(group.appId) ? ( +
+ + + +
+ } + meta={ +
+ {group.totalTps.toFixed(1)} msg/s + {group.totalActiveRoutes}/{group.totalRoutes} routes + + + +
+ } + footer={ + group.deadCount > 0 ? ( +
+ + + Single point of failure —{' '} + {group.deadCount === group.instances.length + ? 'no redundancy' + : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`} + +
+ ) : undefined + } + > + + columns={instanceColumns as Column[]} + data={group.instances.map(i => ({ ...i, id: i.instanceId }))} + onRowClick={handleInstanceClick} + pageSize={50} + flush + /> + +
+ ) : ( + toggleAppExpanded(group.appId)} + /> + ), + )} +
+ )} {/* Log + Timeline side by side */}