diff --git a/ui/src/pages/AgentHealth/AgentHealth.module.css b/ui/src/pages/AgentHealth/AgentHealth.module.css index 5f0a9283..37efb44b 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.module.css +++ b/ui/src/pages/AgentHealth/AgentHealth.module.css @@ -13,7 +13,7 @@ /* Stat strip */ .statStrip { display: grid; - grid-template-columns: repeat(5, 1fr) auto; + grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 16px; } @@ -99,6 +99,7 @@ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-bottom: 20px; + position: relative; } /* Group meta row */ @@ -317,25 +318,49 @@ color: var(--card-accent); } -/* Expanded card inside compact grid */ +/* Wrapper for each compact grid cell — anchor for overlay */ +.compactGridCell { + position: relative; +} + +/* Expanded card overlay — floats from the clicked card */ .compactGridExpanded { - grid-column: span 2; + position: absolute; + z-index: 10; + top: 0; + left: 0; + min-width: 500px; + background: var(--bg-body); + border-radius: var(--radius-md); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + padding: 4px; } /* Expand/collapse animation wrapper */ .expandWrapper { overflow: hidden; - transition: max-height 200ms ease, opacity 150ms ease; + transition: opacity 200ms ease, transform 200ms ease; } .expandWrapperCollapsed { - max-height: 0; opacity: 0; + transform: scaleY(0.95); + transform-origin: top; } .expandWrapperExpanded { - max-height: 1000px; opacity: 1; + transform: scaleY(1); + transform-origin: top; +} + +/* View toolbar — between stat strip and cards grid */ +.viewToolbar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin-bottom: 12px; } /* View mode toggle */ diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index dcb6048a..bf0ea7af 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -289,7 +289,7 @@ export default function AgentHealth() { const agentList = agents ?? []; - const groups = useMemo(() => groupByApp(agentList), [agentList]); + const groups = useMemo(() => groupByApp(agentList).sort((a, b) => a.appId.localeCompare(b.appId)), [agentList]); // Aggregate stats const totalInstances = agentList.length; @@ -515,6 +515,10 @@ export default function AgentHealth() { accent={deadCount > 0 ? 'error' : 'success'} detail={deadCount > 0 ? 'requires attention' : 'all healthy'} /> + + + {/* View toolbar */} +
-
- } - 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 - /> - -
- ) : ( + {groups.map((group) => ( +
animateToggle(group.appId)} /> - ), - )} + {expandedApps.has(group.appId) && ( +
{ + if (!el) return; + // Constrain overlay within viewport + const rect = el.getBoundingClientRect(); + const vw = document.documentElement.clientWidth; + if (rect.right > vw - 16) { + el.style.left = 'auto'; + el.style.right = '0'; + } + if (rect.bottom > document.documentElement.clientHeight) { + const overflow = rect.bottom - document.documentElement.clientHeight + 16; + el.style.maxHeight = `${rect.height - overflow}px`; + el.style.overflowY = 'auto'; + } + }} + className={`${styles.compactGridExpanded} ${styles.expandWrapper} ${ + animatingApps.get(group.appId) === 'expanding' + ? styles.expandWrapperCollapsed + : animatingApps.get(group.appId) === 'collapsing' + ? styles.expandWrapperCollapsed + : styles.expandWrapperExpanded + }`} + > + + + +
+ } + 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 + /> + +
+ )} + + ))} )}