claude revamped the ui

This commit is contained in:
lda committed 2026-04-30 22:15:55 +07:00 Verified
1 parent 181f2733b1
commit 42af1899b4
20 files changed
+2029 -770

No files matched your search

+68 -429
View File
@@ -1,37 +1,26 @@
import { useEffect, useMemo, useState } from "react";
import {
Check,
Copy,
Inbox,
MoreHorizontal,
RefreshCw,
Search,
Zap,
} from "lucide-react";
import { toast } from "sonner";
import {
attachDeviceIdentifier,
createKnownDevice,
fetchFleetDevices,
fetchKnownDevices,
mergeKnownDevice,
refreshFleetDevices,
wakeFleetDevice,
type Agent,
type FleetDevice,
type FleetWakeRoute,
type KnownDevice,
} from "@/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -45,7 +34,24 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
FilterSelect,
MobileLabel,
PresenceBadge,
} from "@/pages/fleet/FleetComponents";
import { FleetDeviceDetailsDialog } from "@/pages/fleet/FleetDeviceDetailsDialog";
import {
agentLabel,
formatSeen,
knownFilters,
presenceFilters,
presenceRank,
summarize,
type KnownFilter,
type SortDir,
type SortKey,
} from "@/pages/fleet/utils";
type Props = {
agents: Agent[];
@@ -54,20 +60,6 @@ type Props = {
onAfterWake?: () => Promise<void>;
};
type SortKey = "name" | "presence" | "last_seen" | "agents" | "ip" | "mac";
type SortDir = "asc" | "desc";
type KnownFilter = "all" | "known" | "unknown";
const presenceFilters = [
"all",
"online",
"likely_online",
"unknown",
"offline",
] as const;
const knownFilters: KnownFilter[] = ["all", "known", "unknown"];
export function DevicesPage({ agents, onAfterWake }: Props) {
const [devices, setDevices] = useState<FleetDevice[]>([]);
const [knownDevices, setKnownDevices] = useState<KnownDevice[]>([]);
@@ -117,14 +109,15 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
try {
const result = await refreshFleetDevices();
const failed = result.agents.filter((agent) => agent.status !== "ok");
setStatus(
failed.length
? `Stored ${result.total_accepted} observations; ${failed.length} agent refresh failed`
: `Stored ${result.total_accepted} observations from ${result.agents.length} connected agents`,
);
const msg = failed.length
? `Stored ${result.total_accepted} observations; ${failed.length} agent refresh failed`
: `Stored ${result.total_accepted} observations from ${result.agents.length} connected agents`;
setStatus(msg);
toast.success(msg);
await loadFleet();
} catch (err) {
setError(String(err));
toast.error("Fleet refresh failed", { description: String(err) });
} finally {
setRefreshing(false);
}
@@ -140,13 +133,14 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
deviceKey: device.device_key,
routeId,
});
setStatus(
`Wake sent via ${agentLabel(result.route.agent_id, result.route.nickname)} (${result.route.mac ?? "no mac"})`,
);
const msg = `Wake sent via ${agentLabel(result.route.agent_id, result.route.nickname)} (${result.route.mac ?? "no mac"})`;
toast.success(msg);
setStatus(msg);
if (onAfterWake) await onAfterWake();
await loadFleet();
} catch (err) {
setError(String(err));
toast.error("Wake failed", { description: String(err) });
} finally {
setWakeBusyKey("");
}
@@ -158,6 +152,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
try {
await navigator.clipboard.writeText(text);
setCopied(label);
toast.success(`Copied ${label}`);
} catch {
const area = document.createElement("textarea");
area.value = text;
@@ -168,6 +163,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
document.execCommand("copy");
document.body.removeChild(area);
setCopied(label);
toast.success(`Copied ${label}`);
}
}
@@ -337,32 +333,44 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
</div>
)}
<div className="grid gap-2">
<div className="hidden grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] gap-2 rounded-md border bg-muted/60 px-3 py-2 text-sm font-medium xl:grid">
<span>Device</span>
<span>Presence</span>
<span>IPs</span>
<span>MACs</span>
<span>Agents</span>
<span>Last seen</span>
<span className="text-right">Actions</span>
{/* Skeleton loading */}
{loading && !devices.length ? (
<div className="grid gap-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-md" />
))}
</div>
{sortedDevices.map((device) => (
<FleetDeviceRow
key={device.device_key}
device={device}
busy={wakeBusyKey === device.device_key}
onWake={() => void wakeDevice(device)}
onDetails={() => setDetails(device)}
onCopy={(label, value) => void copyValue(label, value)}
/>
))}
{!sortedDevices.length && (
<div className="rounded-md border bg-card p-4 text-sm text-muted-foreground">
No devices match the current filters.
) : (
<div className="grid gap-2">
<div className="hidden grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] gap-2 rounded-md border bg-muted/60 px-3 py-2 text-sm font-medium xl:grid">
<span>Device</span>
<span>Presence</span>
<span>IPs</span>
<span>MACs</span>
<span>Agents</span>
<span>Last seen</span>
<span className="text-right">Actions</span>
</div>
)}
</div>
{sortedDevices.map((device) => (
<FleetDeviceRow
key={device.device_key}
device={device}
busy={wakeBusyKey === device.device_key}
onWake={() => void wakeDevice(device)}
onDetails={() => setDetails(device)}
onCopy={(label, value) => void copyValue(label, value)}
/>
))}
{!sortedDevices.length && (
<div className="flex flex-col items-center justify-center rounded-md border bg-card py-12 text-center">
<Inbox className="size-10 text-muted-foreground/30" />
<p className="mt-3 text-sm text-muted-foreground">
No devices match the current filters
</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
@@ -471,372 +479,3 @@ function FleetDeviceRow({
</div>
);
}
function FleetDeviceDetailsDialog({
device,
knownDevices,
open,
busy,
onOpenChange,
onWake,
onCopy,
onChanged,
}: {
device: FleetDevice | null;
knownDevices: KnownDevice[];
open: boolean;
busy: boolean;
onOpenChange: (open: boolean) => void;
onWake: (routeId?: string | null) => void;
onCopy: (label: string, value: string) => void;
onChanged: () => void;
}) {
const [displayName, setDisplayName] = useState("");
const [targetDeviceId, setTargetDeviceId] = useState("");
const [routeId, setRouteId] = useState("");
const [error, setError] = useState("");
useEffect(() => {
setDisplayName(device?.display_name ?? "");
setTargetDeviceId("");
setRouteId(device?.recommended_route?.route_id ?? "");
setError("");
}, [device]);
if (!device) return null;
const currentDevice = device;
const identifiers = identifiersFor(currentDevice);
const otherKnownDevices = knownDevices.filter(
(known) => known.device_id !== currentDevice.known_device?.device_id,
);
async function createRemembered() {
setError("");
try {
await createKnownDevice({
display_name: displayName.trim() || currentDevice.display_name,
pinned: true,
identifiers,
});
onChanged();
} catch (err) {
setError(String(err));
}
}
async function attachToExisting() {
if (!targetDeviceId) return;
setError("");
try {
for (const identifier of identifiers) {
await attachDeviceIdentifier(targetDeviceId, identifier);
}
onChanged();
} catch (err) {
setError(String(err));
}
}
async function mergeIntoExisting() {
if (!targetDeviceId || !currentDevice.known_device) return;
setError("");
try {
await mergeKnownDevice(
targetDeviceId,
currentDevice.known_device.device_id,
);
onChanged();
onOpenChange(false);
} catch (err) {
setError(String(err));
}
}
const selectedRoute =
device.route_candidates.find((route) => route.route_id === routeId) ??
device.recommended_route;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[92vh] overflow-auto sm:max-w-3xl">
<DialogHeader>
<div className="flex min-w-0 items-start justify-between gap-3 pr-8">
<div className="min-w-0">
<DialogTitle className="truncate">
{device.display_name}
</DialogTitle>
<DialogDescription>
{device.known_device
? "Remembered fleet device"
: "Observed fleet device"}
</DialogDescription>
</div>
<PresenceBadge presence={device.presence} />
</div>
</DialogHeader>
{error && (
<pre className="max-h-40 overflow-auto rounded-md border border-destructive/60 bg-destructive/10 p-3 text-xs text-destructive">
{error}
</pre>
)}
<div className="grid gap-3 md:grid-cols-2">
<DetailBlock label="IPs" values={device.ips} onCopy={onCopy} />
<DetailBlock label="MACs" values={device.macs} onCopy={onCopy} />
<DetailBlock
label="Hostnames"
values={device.hostnames}
onCopy={onCopy}
/>
<DetailBlock
label="Agents"
values={device.agents.map((agent) =>
agentLabel(agent.agent_id, agent.nickname),
)}
onCopy={onCopy}
/>
</div>
<Separator />
<div className="grid gap-2">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium">Wake route</h3>
<Button
size="sm"
onClick={() => onWake(selectedRoute?.route_id ?? null)}
disabled={busy || !selectedRoute?.wakeable}
>
<Zap className="size-4" aria-hidden />
{busy ? "Waking" : "Wake"}
</Button>
</div>
<Select
value={routeId}
onValueChange={(value) => setRouteId(value ?? "")}
>
<SelectTrigger>
<span>
{selectedRoute
? routeLabel(selectedRoute)
: "No connected MAC-backed route"}
</span>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{device.route_candidates.map((route) => (
<SelectItem key={route.route_id} value={route.route_id}>
{routeLabel(route)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator />
<div className="grid gap-3">
<h3 className="text-sm font-medium">Remember / merge</h3>
{!device.known_device && (
<div className="grid gap-2 md:grid-cols-[minmax(0,1fr)_auto]">
<Input
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="Device name"
/>
<Button
onClick={() => void createRemembered()}
disabled={!identifiers.length}
>
Remember device
</Button>
</div>
)}
<div className="grid gap-2 md:grid-cols-[minmax(0,1fr)_auto_auto]">
<Select
value={targetDeviceId}
onValueChange={(value) => setTargetDeviceId(value ?? "")}
>
<SelectTrigger>
<span>
{targetDeviceId
? knownDevices.find(
(known) => known.device_id === targetDeviceId,
)?.display_name
: "Select known device"}
</span>
</SelectTrigger>
<SelectContent
className="max-w-[min(92vw,30rem)]"
alignItemWithTrigger={false}
>
{otherKnownDevices.map((known) => (
<SelectItem key={known.device_id} value={known.device_id}>
{known.display_name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => void attachToExisting()}
disabled={!targetDeviceId || !identifiers.length}
>
Attach IDs
</Button>
<Button
variant="outline"
onClick={() => void mergeIntoExisting()}
disabled={!targetDeviceId || !device.known_device}
>
Merge
</Button>
</div>
<p className="text-xs text-muted-foreground">
Proposed identifiers:{" "}
{identifiers.map((id) => `${id.kind}:${id.value}`).join(", ") ||
"-"}
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function FilterSelect({
label,
value,
values,
onChange,
}: {
label: string;
value: string;
values: readonly string[];
onChange: (value: string) => void;
}) {
return (
<label className="grid gap-1 text-sm text-muted-foreground">
<span>{label}</span>
<Select value={value} onValueChange={(next) => next && onChange(next)}>
<SelectTrigger>
<span>{value.replace("_", " ")}</span>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{values.map((item) => (
<SelectItem key={item} value={item}>
{item.replace("_", " ")}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
);
}
function DetailBlock({
label,
values,
onCopy,
}: {
label: string;
values: string[];
onCopy: (label: string, value: string) => void;
}) {
return (
<div className="rounded-md border bg-muted/30 p-3">
<div className="mb-2 text-xs font-medium uppercase text-muted-foreground">
{label}
</div>
<div className="grid gap-1">
{values.length ? (
values.map((value) => (
<button
key={value}
type="button"
className="flex min-w-0 items-center justify-between gap-2 rounded px-1 py-0.5 text-left hover:bg-accent"
onClick={() => onCopy(label, value)}
>
<span className="min-w-0 truncate">{value}</span>
<Copy
className="size-3.5 shrink-0 text-muted-foreground"
aria-hidden
/>
</button>
))
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</div>
</div>
);
}
function PresenceBadge({ presence }: { presence: string }) {
return (
<Badge variant={presence === "offline" ? "outline" : "secondary"}>
{presence.replace("_", " ")}
</Badge>
);
}
function MobileLabel({ label }: { label: string }) {
return (
<span className="mb-1 block text-xs font-medium text-muted-foreground xl:hidden">
{label}
</span>
);
}
function summarize(values: string[]): string {
if (!values.length) return "-";
if (values.length <= 2) return values.join(", ");
return `${values[0]}, ${values[1]} (+${values.length - 2})`;
}
function formatSeen(value: number | null): string {
if (!value) return "-";
const seconds = Math.max(0, Math.floor(Date.now() / 1000) - value);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 48) return `${hours}h ago`;
return new Date(value * 1000).toLocaleString();
}
function presenceRank(presence: string): number {
if (presence === "online") return 4;
if (presence === "likely_online") return 3;
if (presence === "unknown") return 2;
if (presence === "offline") return 1;
return 0;
}
function identifiersFor(
device: FleetDevice,
): { kind: string; value: string }[] {
return [
...device.macs.map((value) => ({ kind: "mac", value })),
...device.ips.map((value) => ({ kind: "ip", value })),
];
}
function agentLabel(agentId: string, nickname?: string | null): string {
const trimmed = nickname?.trim();
return trimmed ? trimmed : agentId;
}
function routeLabel(route: FleetWakeRoute): string {
const status = route.connected ? "connected" : "offline";
const target = route.mac
? `${route.mac}${route.ip ? ` / ${route.ip}` : ""}`
: (route.ip ?? "-");
return `${agentLabel(route.agent_id, route.nickname)} · ${target} · ${route.source} · ${status}`;
}