Files
wireguard-ui/src/pages/admin/clients/(sections)/clients-table-section.tsx
T
2026-09-20 11:15:22 +01:00

151 lines
6.1 KiB
TypeScript

import { useMemo, useState, type Dispatch, type SetStateAction } from "react";
import Search from "@/src/components/twui/elements/Search";
import Loading from "@/src/components/twui/elements/Loading";
import AdminCard from "@/src/components/general/admin-card";
import ClientRow from "@/src/components/general/client-row";
import H2 from "@/src/components/twui/layout/H2";
import Span from "@/src/components/twui/layout/Span";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import ClientFormModal from "../(partials)/client-form-modal";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_CLIENT_RULES,
} from "@/db/types/db";
type Props = {
addOpen: boolean;
setAddOpen: Dispatch<SetStateAction<boolean>>;
};
const thClass =
"px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " +
"text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap";
const thRightClass = `${thClass} text-right`;
export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
const [query, setQuery] = useState("");
const { res } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
table: "clients",
sql_query: {
order: { field: "created_at", strategy: "DESC" },
limit: 100,
},
});
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>({
table: "client_rules",
sql_query: {
limit: 500,
},
});
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
for (let i = 0; i < (client_rules || []).length; i++) {
const rule = client_rules?.[i];
const client_id = Number(rule?.client_id);
if (!rule || !client_id) {
continue;
}
const existing = map.get(client_id) || [];
existing.push(rule);
map.set(client_id, existing);
}
return map;
}, [client_rules]);
const filtered = useMemo(() => {
const clients = res || [];
const q = query.trim().toLowerCase();
if (!q) {
return clients;
}
return clients.filter((client) =>
[client.name, client.wg_ip_address, client.allowed_ips].some(
(value) => (value || "").toLowerCase().includes(q),
),
);
}, [res, query]);
return (
<AdminCard className="w-full overflow-hidden">
<Row className="justify-between gap-3 px-5 h-12 flex-wrap">
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
Clients
<Span className="tabular text-foreground-light/40 dark:text-foreground-dark/40 ml-2">
{String(filtered.length)}
</Span>
</H2>
<Search
no_search_button
placeholder="Search clients…"
changeHandler={(value) => setQuery(value || "")}
inputProps={{
className: "!text-[13.5px]",
wrapperProps: {
className:
"!py-[5px] !min-h-[30px] w-[220px] bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]",
},
}}
/>
</Row>
{res === undefined ? (
<Stack center className="w-full py-10">
<Loading />
</Stack>
) : filtered.length ? (
<div className="overflow-x-auto">
<table className="w-full min-w-[940px]">
<thead>
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
<th className={thClass}>Client</th>
<th className={thClass}>Host</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{filtered.map((client) => (
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(
Number(client.id),
)
: undefined
}
/>
))}
</tbody>
</table>
</div>
) : (
<Stack center className="w-full justify-center py-14 px-6 text-center gap-1">
<Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
{query ? "No clients found" : "No clients yet"}
</Span>
<Span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
{query
? "Try adjusting your search terms"
: "Add a client to get started"}
</Span>
</Stack>
)}
<ClientFormModal open={addOpen} setOpen={setAddOpen} />
</AdminCard>
);
}