system-monitor: docker grid view + name/image filter

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 10:45:36 +00:00
co-authored by Claude Opus 4.8
parent f3108f6d83
commit 41ebd73c2d
@@ -12,6 +12,7 @@ export const DockerView = () => {
const [containers, setContainers] = useState<Container[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [selected, setSelected] = useState<{ id: string; name: string } | null>(null);
const [filter, setFilter] = useState('');
const alive = useRef(true);
useEffect(() => {
@@ -43,34 +44,45 @@ export const DockerView = () => {
/>
);
const q = filter.trim().toLowerCase();
const filtered = (containers ?? []).filter((c) => !q || c.name.toLowerCase().includes(q) || c.image.toLowerCase().includes(q));
return (
<div className="h-full w-full overflow-y-auto p-4 md:p-6">
<div className="mx-auto max-w-5xl">
<div className="mb-3 text-xs text-muted-foreground">docker · {containers?.length ?? 0} running · click a container for live logs</div>
<div className="mx-auto w-full max-w-7xl">
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">
docker · {filtered.length}/{containers?.length ?? 0} · click a container for live logs
</span>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="filter name or image…"
className="w-56 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
{error && <div className="mb-3 rounded-lg border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-500">{error}</div>}
<div className="flex flex-col gap-2">
{(containers ?? []).map((c) => (
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4">
{filtered.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setSelected({ id: c.id, name: c.name })}
className="rounded-xl border border-border bg-card p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted/40"
className="flex flex-col rounded-lg border border-border bg-card p-2.5 text-left transition-colors hover:border-primary/40 hover:bg-muted/40"
>
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<span className={`inline-block h-2 w-2 shrink-0 rounded-full ${stateColor(c.state)}`} />
<span className="truncate font-medium text-foreground">{c.name}</span>
</div>
<span className="shrink-0 text-xs text-muted-foreground">{c.status}</span>
<div className="flex min-w-0 items-center gap-2">
<span className={`inline-block h-2 w-2 shrink-0 rounded-full ${stateColor(c.state)}`} />
<span className="truncate text-sm font-medium text-foreground">{c.name}</span>
</div>
<div className="mt-1 truncate text-xs text-muted-foreground" title={c.image}>{c.image}</div>
{c.ports && (
<div className="mt-1 truncate font-mono text-xs text-muted-foreground/70" title={c.ports}>{c.ports}</div>
)}
<div className="mt-0.5 truncate text-[11px] text-muted-foreground/70" title={c.status}>{c.status}</div>
{c.ports && <div className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground/60" title={c.ports}>{c.ports}</div>}
</button>
))}
{containers && containers.length === 0 && <p className="text-sm text-muted-foreground">no running containers</p>}
</div>
{containers && filtered.length === 0 && (
<p className="mt-3 text-sm text-muted-foreground">{q ? 'no matches' : 'no running containers'}</p>
)}
</div>
</div>
);