feat: add sort buttons and fix filter placeholder
Some checks failed
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 2m12s
CI / cleanup-branch (push) Has been skipped
CI / docker (pull_request) Has been skipped
CI / build (push) Successful in 2m13s
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 2m12s
CI / cleanup-branch (push) Has been skipped
CI / docker (pull_request) Has been skipped
CI / build (push) Successful in 2m13s
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
Add sort buttons (Status, Name, TPS, CPU, Heartbeat) to the toolbar, right-aligned. Clicking toggles asc/desc, second sort criterion is always name. Status sorts error > warning > success. Fix trailing unicode escape in filter placeholder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -428,6 +428,42 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sort buttons */
|
||||||
|
.sortGroup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 150ms ease, border-color 150ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortBtn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortBtnActive {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-subtle);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* View mode toggle */
|
/* View mode toggle */
|
||||||
.viewToggle {
|
.viewToggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown } from 'lucide-react';
|
import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText,
|
StatCard, StatusDot, Badge, MonoText,
|
||||||
GroupCard, DataTable, EventFeed,
|
GroupCard, DataTable, EventFeed,
|
||||||
@@ -285,6 +285,19 @@ export default function AgentHealth() {
|
|||||||
const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv);
|
const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv);
|
||||||
|
|
||||||
const [appFilter, setAppFilter] = useState('');
|
const [appFilter, setAppFilter] = useState('');
|
||||||
|
type AppSortKey = 'status' | 'name' | 'tps' | 'cpu' | 'heartbeat';
|
||||||
|
const [appSortKey, setAppSortKey] = useState<AppSortKey>('name');
|
||||||
|
const [appSortAsc, setAppSortAsc] = useState(true);
|
||||||
|
|
||||||
|
const cycleSort = useCallback((key: AppSortKey) => {
|
||||||
|
if (appSortKey === key) {
|
||||||
|
setAppSortAsc((prev) => !prev);
|
||||||
|
} else {
|
||||||
|
setAppSortKey(key);
|
||||||
|
setAppSortAsc(key === 'name'); // name defaults asc, others desc
|
||||||
|
}
|
||||||
|
}, [appSortKey]);
|
||||||
|
|
||||||
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'
|
||||||
@@ -306,9 +319,34 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
const agentList = agents ?? [];
|
const agentList = agents ?? [];
|
||||||
|
|
||||||
const allGroups = useMemo(() => groupByApp(agentList).sort((a, b) => a.appId.localeCompare(b.appId)), [agentList]);
|
const allGroups = useMemo(() => groupByApp(agentList), [agentList]);
|
||||||
|
|
||||||
|
const sortedGroups = useMemo(() => {
|
||||||
|
const healthPriority = (g: AppGroup) => g.deadCount > 0 ? 0 : g.staleCount > 0 ? 1 : 2;
|
||||||
|
const nameCmp = (a: AppGroup, b: AppGroup) => a.appId.localeCompare(b.appId);
|
||||||
|
|
||||||
|
const sorted = [...allGroups].sort((a, b) => {
|
||||||
|
let cmp = 0;
|
||||||
|
switch (appSortKey) {
|
||||||
|
case 'status': cmp = healthPriority(a) - healthPriority(b); break;
|
||||||
|
case 'name': cmp = a.appId.localeCompare(b.appId); break;
|
||||||
|
case 'tps': cmp = a.totalTps - b.totalTps; break;
|
||||||
|
case 'cpu': cmp = a.maxCpu - b.maxCpu; break;
|
||||||
|
case 'heartbeat': {
|
||||||
|
const ha = latestHeartbeat(a) ?? '';
|
||||||
|
const hb = latestHeartbeat(b) ?? '';
|
||||||
|
cmp = ha < hb ? -1 : ha > hb ? 1 : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!appSortAsc) cmp = -cmp;
|
||||||
|
return cmp !== 0 ? cmp : nameCmp(a, b);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}, [allGroups, appSortKey, appSortAsc]);
|
||||||
|
|
||||||
const appFilterLower = appFilter.toLowerCase();
|
const appFilterLower = appFilter.toLowerCase();
|
||||||
const groups = appFilterLower ? allGroups.filter((g) => g.appId.toLowerCase().includes(appFilterLower)) : allGroups;
|
const groups = appFilterLower ? sortedGroups.filter((g) => g.appId.toLowerCase().includes(appFilterLower)) : sortedGroups;
|
||||||
|
|
||||||
// Aggregate stats
|
// Aggregate stats
|
||||||
const totalInstances = agentList.length;
|
const totalInstances = agentList.length;
|
||||||
@@ -673,7 +711,7 @@ export default function AgentHealth() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.appFilterInput}
|
className={styles.appFilterInput}
|
||||||
placeholder="Filter apps\u2026"
|
placeholder="Filter apps..."
|
||||||
value={appFilter}
|
value={appFilter}
|
||||||
onChange={(e) => setAppFilter(e.target.value)}
|
onChange={(e) => setAppFilter(e.target.value)}
|
||||||
aria-label="Filter applications"
|
aria-label="Filter applications"
|
||||||
@@ -689,6 +727,21 @@ export default function AgentHealth() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.sortGroup}>
|
||||||
|
{([['status', 'Status'], ['name', 'Name'], ['tps', 'TPS'], ['cpu', 'CPU'], ['heartbeat', 'Heartbeat']] as const).map(([key, label]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
className={`${styles.sortBtn} ${appSortKey === key ? styles.sortBtnActive : ''}`}
|
||||||
|
onClick={() => cycleSort(key)}
|
||||||
|
title={`Sort by ${label}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{appSortKey === key && (
|
||||||
|
appSortAsc ? <ArrowUp size={10} /> : <ArrowDown size={10} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user