feat: move toggle to toolbar, sort apps by name, overlay expand
- Move expand/collapse toggle from stat strip to dedicated toolbar below KPIs - Sort app groups alphabetically by name - Expanded card overlays from clicked card position instead of pushing other cards down - Viewport constraint: overlay flips right-alignment and limits height when near edges Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
/* Stat strip */
|
/* Stat strip */
|
||||||
.statStrip {
|
.statStrip {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr) auto;
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Group meta row */
|
/* Group meta row */
|
||||||
@@ -317,25 +318,49 @@
|
|||||||
color: var(--card-accent);
|
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 {
|
.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 */
|
/* Expand/collapse animation wrapper */
|
||||||
.expandWrapper {
|
.expandWrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 200ms ease, opacity 150ms ease;
|
transition: opacity 200ms ease, transform 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandWrapperCollapsed {
|
.expandWrapperCollapsed {
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: scaleY(0.95);
|
||||||
|
transform-origin: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandWrapperExpanded {
|
.expandWrapperExpanded {
|
||||||
max-height: 1000px;
|
|
||||||
opacity: 1;
|
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 */
|
/* View mode toggle */
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
const agentList = agents ?? [];
|
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
|
// Aggregate stats
|
||||||
const totalInstances = agentList.length;
|
const totalInstances = agentList.length;
|
||||||
@@ -515,6 +515,10 @@ export default function AgentHealth() {
|
|||||||
accent={deadCount > 0 ? 'error' : 'success'}
|
accent={deadCount > 0 ? 'error' : 'success'}
|
||||||
detail={deadCount > 0 ? 'requires attention' : 'all healthy'}
|
detail={deadCount > 0 ? 'requires attention' : 'all healthy'}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View toolbar */}
|
||||||
|
<div className={styles.viewToolbar}>
|
||||||
<div className={styles.viewToggle}>
|
<div className={styles.viewToggle}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.viewToggleBtn} ${viewMode === 'compact' ? styles.viewToggleBtnActive : ''}`}
|
className={`${styles.viewToggleBtn} ${viewMode === 'compact' ? styles.viewToggleBtnActive : ''}`}
|
||||||
@@ -702,85 +706,99 @@ export default function AgentHealth() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.compactGrid}>
|
<div className={styles.compactGrid}>
|
||||||
{groups.map((group) =>
|
{groups.map((group) => (
|
||||||
expandedApps.has(group.appId) ? (
|
<div key={group.appId} className={styles.compactGridCell}>
|
||||||
<div
|
|
||||||
key={group.appId}
|
|
||||||
className={`${styles.compactGridExpanded} ${styles.expandWrapper} ${
|
|
||||||
animatingApps.get(group.appId) === 'expanding'
|
|
||||||
? styles.expandWrapperCollapsed
|
|
||||||
: animatingApps.get(group.appId) === 'collapsing'
|
|
||||||
? styles.expandWrapperCollapsed
|
|
||||||
: styles.expandWrapperExpanded
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<GroupCard
|
|
||||||
title={group.appId}
|
|
||||||
accent={appHealth(group)}
|
|
||||||
headerRight={
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Badge
|
|
||||||
label={`${group.liveCount}/${group.instances.length} LIVE`}
|
|
||||||
color={appHealth(group)}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={styles.collapseBtn}
|
|
||||||
onClick={() => animateToggle(group.appId)}
|
|
||||||
title="Collapse"
|
|
||||||
>
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
meta={
|
|
||||||
<div className={styles.groupMeta}>
|
|
||||||
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
|
|
||||||
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
|
||||||
<span>
|
|
||||||
<StatusDot
|
|
||||||
variant={
|
|
||||||
appHealth(group) === 'success'
|
|
||||||
? 'live'
|
|
||||||
: appHealth(group) === 'warning'
|
|
||||||
? 'stale'
|
|
||||||
: 'dead'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
footer={
|
|
||||||
group.deadCount > 0 ? (
|
|
||||||
<div className={styles.alertBanner}>
|
|
||||||
<span className={styles.alertIcon}>⚠</span>
|
|
||||||
<span>
|
|
||||||
Single point of failure —{' '}
|
|
||||||
{group.deadCount === group.instances.length
|
|
||||||
? 'no redundancy'
|
|
||||||
: `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DataTable<AgentInstance & { id: string }>
|
|
||||||
columns={instanceColumns as Column<AgentInstance & { id: string }>[]}
|
|
||||||
data={group.instances.map(i => ({ ...i, id: i.instanceId }))}
|
|
||||||
onRowClick={handleInstanceClick}
|
|
||||||
pageSize={50}
|
|
||||||
flush
|
|
||||||
/>
|
|
||||||
</GroupCard>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<CompactAppCard
|
<CompactAppCard
|
||||||
key={group.appId}
|
|
||||||
group={group}
|
group={group}
|
||||||
onExpand={() => animateToggle(group.appId)}
|
onExpand={() => animateToggle(group.appId)}
|
||||||
/>
|
/>
|
||||||
),
|
{expandedApps.has(group.appId) && (
|
||||||
)}
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
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
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<GroupCard
|
||||||
|
title={group.appId}
|
||||||
|
accent={appHealth(group)}
|
||||||
|
headerRight={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Badge
|
||||||
|
label={`${group.liveCount}/${group.instances.length} LIVE`}
|
||||||
|
color={appHealth(group)}
|
||||||
|
variant="filled"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={styles.collapseBtn}
|
||||||
|
onClick={() => animateToggle(group.appId)}
|
||||||
|
title="Collapse"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
meta={
|
||||||
|
<div className={styles.groupMeta}>
|
||||||
|
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
|
||||||
|
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
||||||
|
<span>
|
||||||
|
<StatusDot
|
||||||
|
variant={
|
||||||
|
appHealth(group) === 'success'
|
||||||
|
? 'live'
|
||||||
|
: appHealth(group) === 'warning'
|
||||||
|
? 'stale'
|
||||||
|
: 'dead'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
group.deadCount > 0 ? (
|
||||||
|
<div className={styles.alertBanner}>
|
||||||
|
<span className={styles.alertIcon}>⚠</span>
|
||||||
|
<span>
|
||||||
|
Single point of failure —{' '}
|
||||||
|
{group.deadCount === group.instances.length
|
||||||
|
? 'no redundancy'
|
||||||
|
: `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DataTable<AgentInstance & { id: string }>
|
||||||
|
columns={instanceColumns as Column<AgentInstance & { id: string }>[]}
|
||||||
|
data={group.instances.map(i => ({ ...i, id: i.instanceId }))}
|
||||||
|
onRowClick={handleInstanceClick}
|
||||||
|
pageSize={50}
|
||||||
|
flush
|
||||||
|
/>
|
||||||
|
</GroupCard>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user