feat: add app name filter to runtime toolbar
Some checks failed
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 2m9s
CI / cleanup-branch (push) Has been skipped
CI / docker (pull_request) Has been skipped
CI / build (push) Successful in 2m10s
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-16 15:16:28 +02:00
parent f4811359e1
commit e346b9bb9d
2 changed files with 74 additions and 6 deletions

View File

@@ -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;

View File

@@ -284,6 +284,7 @@ export default function AgentHealth() {
const [eventRefreshTo, setEventRefreshTo] = useState<string | undefined>();
const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv);
const [appFilter, setAppFilter] = useState('');
const [logSearch, setLogSearch] = useState('');
const [logLevels, setLogLevels] = useState<Set<string>>(new Set());
const [logSource, setLogSource] = useState<string>(''); // '' = 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() {
/>
<StatCard
label="Applications"
value={String(groups.length)}
value={String(allGroups.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
<StatusDot variant="live" /> {allGroups.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
<StatusDot variant="stale" /> {allGroups.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
<StatusDot variant="dead" /> {allGroups.filter((g) => g.deadCount > 0).length} critical
</span>
</span>
}
@@ -666,6 +669,26 @@ export default function AgentHealth() {
<List size={14} />
</button>
</div>
<div className={styles.appFilterWrap}>
<input
type="text"
className={styles.appFilterInput}
placeholder="Filter apps\u2026"
value={appFilter}
onChange={(e) => setAppFilter(e.target.value)}
aria-label="Filter applications"
/>
{appFilter && (
<button
type="button"
className={styles.appFilterClear}
onClick={() => setAppFilter('')}
aria-label="Clear filter"
>
&times;
</button>
)}
</div>
</div>
)}