From e346b9bb9d3946c9af250585dbb37604b4d8e9b2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:16:28 +0200 Subject: [PATCH] feat: add app name filter to runtime toolbar Text input next to view toggle filters apps by name (case-insensitive substring match). KPI stat strip uses unfiltered counts so totals stay accurate. Clear button on non-empty input. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pages/AgentHealth/AgentHealth.module.css | 47 ++++++++++++++++++- ui/src/pages/AgentHealth/AgentHealth.tsx | 33 +++++++++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/ui/src/pages/AgentHealth/AgentHealth.module.css b/ui/src/pages/AgentHealth/AgentHealth.module.css index cd77cabc..1e060d06 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.module.css +++ b/ui/src/pages/AgentHealth/AgentHealth.module.css @@ -379,10 +379,55 @@ display: flex; align-items: center; justify-content: flex-start; - gap: 8px; + gap: 12px; margin-bottom: 12px; } +/* App name filter */ +.appFilterWrap { + position: relative; + display: flex; + align-items: center; +} + +.appFilterInput { + width: 180px; + height: 28px; + padding: 0 24px 0 8px; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-primary); + font-size: 12px; + font-family: var(--font-body); + outline: none; + transition: border-color 150ms ease; +} + +.appFilterInput::placeholder { + color: var(--text-muted); +} + +.appFilterInput:focus { + border-color: var(--running); +} + +.appFilterClear { + position: absolute; + right: 4px; + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 14px; + line-height: 1; + padding: 2px; +} + +.appFilterClear:hover { + color: var(--text-primary); +} + /* View mode toggle */ .viewToggle { display: flex; diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index c3f1ce04..afb3511e 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -284,6 +284,7 @@ export default function AgentHealth() { const [eventRefreshTo, setEventRefreshTo] = useState(); const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv); + const [appFilter, setAppFilter] = useState(''); const [logSearch, setLogSearch] = useState(''); const [logLevels, setLogLevels] = useState>(new Set()); const [logSource, setLogSource] = useState(''); // '' = all, 'app', 'agent' @@ -305,7 +306,9 @@ export default function AgentHealth() { const agentList = agents ?? []; - const groups = useMemo(() => groupByApp(agentList).sort((a, b) => a.appId.localeCompare(b.appId)), [agentList]); + const allGroups = useMemo(() => groupByApp(agentList).sort((a, b) => a.appId.localeCompare(b.appId)), [agentList]); + const appFilterLower = appFilter.toLowerCase(); + const groups = appFilterLower ? allGroups.filter((g) => g.appId.toLowerCase().includes(appFilterLower)) : allGroups; // Aggregate stats const totalInstances = agentList.length; @@ -486,18 +489,18 @@ export default function AgentHealth() { /> - {groups.filter((g) => g.deadCount === 0 && g.staleCount === 0).length} healthy + {allGroups.filter((g) => g.deadCount === 0 && g.staleCount === 0).length} healthy - {groups.filter((g) => g.staleCount > 0 && g.deadCount === 0).length} degraded + {allGroups.filter((g) => g.staleCount > 0 && g.deadCount === 0).length} degraded - {groups.filter((g) => g.deadCount > 0).length} critical + {allGroups.filter((g) => g.deadCount > 0).length} critical } @@ -666,6 +669,26 @@ export default function AgentHealth() { +
+ setAppFilter(e.target.value)} + aria-label="Filter applications" + /> + {appFilter && ( + + )} +
)}