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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
margin-bottom: 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 */
|
/* View mode toggle */
|
||||||
.viewToggle {
|
.viewToggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export default function AgentHealth() {
|
|||||||
const [eventRefreshTo, setEventRefreshTo] = useState<string | undefined>();
|
const [eventRefreshTo, setEventRefreshTo] = useState<string | undefined>();
|
||||||
const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv);
|
const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv);
|
||||||
|
|
||||||
|
const [appFilter, setAppFilter] = useState('');
|
||||||
const [logSearch, setLogSearch] = useState('');
|
const [logSearch, setLogSearch] = useState('');
|
||||||
const [logLevels, setLogLevels] = useState<Set<string>>(new Set());
|
const [logLevels, setLogLevels] = useState<Set<string>>(new Set());
|
||||||
const [logSource, setLogSource] = useState<string>(''); // '' = all, 'app', 'agent'
|
const [logSource, setLogSource] = useState<string>(''); // '' = all, 'app', 'agent'
|
||||||
@@ -305,7 +306,9 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
const agentList = agents ?? [];
|
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
|
// Aggregate stats
|
||||||
const totalInstances = agentList.length;
|
const totalInstances = agentList.length;
|
||||||
@@ -486,18 +489,18 @@ export default function AgentHealth() {
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Applications"
|
label="Applications"
|
||||||
value={String(groups.length)}
|
value={String(allGroups.length)}
|
||||||
accent="running"
|
accent="running"
|
||||||
detail={
|
detail={
|
||||||
<span className={styles.breakdown}>
|
<span className={styles.breakdown}>
|
||||||
<span className={styles.bpLive}>
|
<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>
|
||||||
<span className={styles.bpStale}>
|
<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>
|
||||||
<span className={styles.bpDead}>
|
<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>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -666,6 +669,26 @@ export default function AgentHealth() {
|
|||||||
<List size={14} />
|
<List size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user