milestone 9: react spa admin ui, frontend ci and embedded dist

This commit is contained in:
2026-08-02 13:04:09 +02:00
parent 5253c47303
commit 617cc966a2
82 changed files with 11833 additions and 17 deletions
+115
View File
@@ -0,0 +1,115 @@
import { useState } from "react";
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { clientDeleteMutation, clientPrefixesQuery, clientsQuery, groupsQuery } from "@/lib/queries";
import { formatTime } from "@/lib/format";
import type { Client } from "@/lib/types";
import ClientEditDialog from "./ClientEditDialog";
import PrefixesEditor from "./PrefixesEditor";
import InlineError from "@/lib/InlineError";
const cellClass = "px-3 py-2";
const buttonClass =
"rounded border border-zinc-300 px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
export default function ClientsPage() {
const { data: clients } = useSuspenseQuery(clientsQuery());
const { data: prefixes } = useSuspenseQuery(clientPrefixesQuery());
const { data: groups } = useSuspenseQuery(groupsQuery());
const queryClient = useQueryClient();
const deleteMutation = useMutation(clientDeleteMutation(queryClient));
const [editing, setEditing] = useState<Client | null>(null);
const [confirmingId, setConfirmingId] = useState<number | null>(null);
return (
<section>
<h1 className="text-2xl font-semibold">Clients</h1>
{clients.length === 0 ? (
<p className="mt-4 text-zinc-500">
No clients yet. Rows appear automatically as devices on the network make DNS queries there is
nothing to create by hand.
</p>
) : (
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[48rem] text-left text-sm">
<thead>
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-700">
<th className={cellClass}>IP</th>
<th className={cellClass}>Name</th>
<th className={cellClass}>Group</th>
<th className={cellClass}>First seen</th>
<th className={cellClass}>Last seen</th>
<th className={cellClass}>
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{clients.map((client) => (
<tr key={client.id} className="border-b border-zinc-100 dark:border-zinc-800">
<td className={`${cellClass} font-mono`}>{client.ip}</td>
<td className={cellClass}>
{client.name === "" ? <span className="text-zinc-400"></span> : client.name}
{client.hand_edited && (
<span className="ml-2 rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">
edited
</span>
)}
</td>
<td className={cellClass}>{client.group}</td>
<td className={cellClass}>{formatTime(client.first_seen)}</td>
<td className={cellClass}>{formatTime(client.last_seen)}</td>
<td className={`${cellClass} text-right`}>
{confirmingId === client.id ? (
<span className="inline-flex flex-wrap items-center justify-end gap-2">
<span className="text-xs text-zinc-500">
Deleted clients re-materialize on their next DNS query.
</span>
<button
type="button"
onClick={() => {
setConfirmingId(null);
deleteMutation.mutate(client.id);
}}
className={`${buttonClass} text-red-700 dark:text-red-400`}
>
Confirm delete
</button>
<button
type="button"
onClick={() => setConfirmingId(null)}
className={buttonClass}
>
Cancel
</button>
</span>
) : (
<span className="inline-flex gap-2">
<button
type="button"
onClick={() => setEditing(client)}
className={buttonClass}
>
Edit
</button>
<button
type="button"
onClick={() => setConfirmingId(client.id)}
className={`${buttonClass} text-red-700 dark:text-red-400`}
>
Delete
</button>
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<InlineError error={deleteMutation.error} />
{editing !== null && <ClientEditDialog client={editing} groups={groups} onClose={() => setEditing(null)} />}
<PrefixesEditor prefixes={prefixes} groups={groups} />
</section>
);
}