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
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:
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user