Phase 24: Design System v2 — analytics-style UI (0.30.0)
- New /api/dashboard/funnel (5-stage stack health, 30s TTL cache) and /api/dashboard/summary (containers, daily uptime jsonl, ops activity) - Token system (tokens.css + Tailwind sp-* aliases); legacy bg/card/accent remapped onto the tokens; Schibsted Grotesk bundled via fontsource - TopNav pill navigation + AppShell replace the sidebar layout (off-canvas drawer below 1024px); central display-weight page titles - Dashboard redesign: FunnelChart (gradient/hatch SVG waterfall), container count card with per-host bars + Insights chip, UptimeChart, OpsGrid, AiPromptBar; 30/7-day range selector; host sections retained below - Stacks page honours ?q= / ?filter= deep links + new status-filter select Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6464e0677c
commit
34cb215266
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Plus, Search, HardDrive } from "lucide-react";
|
||||
import { Button, Input } from "@/components/ui";
|
||||
@@ -13,11 +13,23 @@ import { useAuthStore } from "@/store/auth";
|
||||
import { useStackActions } from "@/hooks/useStackActions";
|
||||
|
||||
type SortKey = "name" | "status" | "updated";
|
||||
type StatusFilter = "all" | "running" | "stopped" | "attention";
|
||||
|
||||
const STATUS_FILTERS: Record<string, StatusFilter> = {
|
||||
running: "running",
|
||||
stopped: "stopped",
|
||||
attention: "attention",
|
||||
};
|
||||
|
||||
export function Stacks() {
|
||||
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
|
||||
const { busyId, start, stop, restart } = useStackActions();
|
||||
const [q, setQ] = useState("");
|
||||
// The dashboard explore bar deep-links here with ?q= / ?filter=.
|
||||
const [params] = useSearchParams();
|
||||
const [q, setQ] = useState(params.get("q") ?? "");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>(
|
||||
STATUS_FILTERS[params.get("filter") ?? ""] ?? "all"
|
||||
);
|
||||
const [sort, setSort] = useState<SortKey>("name");
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
@@ -50,13 +62,20 @@ export function Stacks() {
|
||||
s.name.toLowerCase().includes(q.toLowerCase()) ||
|
||||
s.id.toLowerCase().includes(q.toLowerCase())
|
||||
);
|
||||
if (statusFilter !== "all") {
|
||||
list = list.filter((s) =>
|
||||
statusFilter === "attention"
|
||||
? s.status === "error" || s.status === "partial"
|
||||
: s.status === statusFilter
|
||||
);
|
||||
}
|
||||
list = [...list].sort((a, b) => {
|
||||
if (sort === "name") return a.name.localeCompare(b.name);
|
||||
if (sort === "status") return a.status.localeCompare(b.status);
|
||||
return b.updated_at.localeCompare(a.updated_at);
|
||||
});
|
||||
return list;
|
||||
}, [data, q, sort]);
|
||||
}, [data, q, statusFilter, sort]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -70,6 +89,16 @@ export function Stacks() {
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as StatusFilter)}
|
||||
className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
|
||||
>
|
||||
<option value="all">All statuses</option>
|
||||
<option value="running">Running</option>
|
||||
<option value="stopped">Stopped</option>
|
||||
<option value="attention">Needs attention</option>
|
||||
</select>
|
||||
<select
|
||||
value={sort}
|
||||
onChange={(e) => setSort(e.target.value as SortKey)}
|
||||
|
||||
Reference in New Issue
Block a user