This commit is contained in:
2026-09-20 11:15:22 +01:00
parent 6e3319b21c
commit 59071f87ad
9 changed files with 125 additions and 184 deletions
+16 -2
View File
@@ -58,6 +58,20 @@ export default function ClientRow({ client, rules }: Props) {
{client.name || "—"}
</Link>
</td>
<td className="px-4 py-[9px]">
{typeof host_id == "number" ? (
<Link
href={`/admin/hosts/${host_id}`}
className="tabular text-[13px] text-foreground-light/60 dark:text-foreground-dark/60 hover:text-secondary dark:hover:text-secondary whitespace-nowrap"
>
{host_id == 0 ? "Main" : `Host #${host_id}`}
</Link>
) : (
<Span className="tabular text-[13px] text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap">
—
</Span>
)}
</td>
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/60 dark:text-foreground-dark/60 whitespace-nowrap">
{client.wg_ip_address || "—"}
</td>
@@ -86,7 +100,7 @@ export default function ClientRow({ client, rules }: Props) {
{formatUnixTimestamp({ timestamp: client.created_at })}
</td>
<td className="px-4 py-[9px] text-right">
<Row className="gap-1.5 items-center">
<Row className="gap-1.5 items-center justify-end">
<Link
href={`/admin/hosts/${host_id}/clients/${client.id}/edit`}
title={`Edit client ${client.name || `#${client.id}`}`}
@@ -109,4 +123,4 @@ export default function ClientRow({ client, rules }: Props) {
</td>
</tr>
);
}
}
@@ -1,54 +0,0 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import type { AddClientFormData } from "@/src/types";
import type { Dispatch, SetStateAction } from "react";
type Params = {
setBusy: Dispatch<SetStateAction<boolean>>;
busy: boolean;
setError: Dispatch<SetStateAction<string | undefined>>;
error?: string;
data: AddClientFormData;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
onCreated?: () => void;
host_id: number;
};
export default async function submitClientForm({
busy,
setBusy,
setError,
error,
data,
open,
setOpen,
onCreated,
host_id,
}: Params) {
setBusy(true);
setError(undefined);
const res = await adminCrudHandler<BUN_SQLITE_WGUI_CLIENTS>({
action: "insert",
table: "clients",
insert_data: [
{
name: data.client_name || "",
wg_ip_address: data.tunnel_ip || "",
allowed_ips: data.allowed_ips || "",
host_id,
},
],
});
setBusy(false);
if (!res.success) {
setError(res.msg || "Could not add client");
return;
}
setOpen(false);
onCreated?.();
}
@@ -0,0 +1,19 @@
import Button from "@/src/components/twui/layout/Button";
type Props = {
host_id: number;
};
export default function ClientFormAction({ host_id }: Props) {
const label =
host_id == 0 ? "Add client to Main Host" : `Add client to Host ${host_id}`;
return (
<Button
title={label}
href={`/admin/hosts/${host_id}/clients/add-client`}
>
{label}
</Button>
);
}
@@ -0,0 +1,27 @@
import type { Dispatch, SetStateAction } from "react";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Select from "@/src/components/twui/form/Select";
type Props = {
hosts: BUN_SQLITE_WGUI_HOSTS[];
host_id: number;
setHostId: Dispatch<SetStateAction<number>>;
};
export default function ClientFormHostSelect({
hosts,
host_id,
setHostId,
}: Props) {
return (
<Select
options={hosts.map((h) => ({
value: (h.id || 0).toString(),
title: h.id == 0 ? `Main Host` : `Host ${h.id}`,
}))}
changeHandler={(v) => {
setHostId(Number(v));
}}
/>
);
}
@@ -1,35 +1,30 @@
import { useState, type Dispatch, type SetStateAction } from "react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import Modal from "@/src/components/twui/elements/Modal";
import Input from "@/src/components/twui/form/Input";
import AdminButton from "@/src/components/general/admin-button";
import Form from "@/src/components/twui/form/Form";
import Stack from "@/src/components/twui/layout/Stack";
import Span from "@/src/components/twui/layout/Span";
import H3 from "@/src/components/twui/layout/H3";
import Row from "@/src/components/twui/layout/Row";
import type { AddClientFormData } from "@/src/types";
import submitClientForm from "../(functions)/submit-client-form";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Select from "@/src/components/twui/form/Select";
import { Globe } from "lucide-react";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import ClientFormTitle from "./client-form-title";
import ClientFormHostSelect from "./client-form-host-select";
import ClientFormAction from "./client-form-action";
type Props = {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
onCreated?: () => void;
};
export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const [host_id, set_host_id] = useState(0);
export default function ClientFormModal({ open, setOpen }: Props) {
const [host_id, setHostId] = useState(0);
const { res: additional_hosts } = useAdminCrudGet<BUN_SQLITE_WGUI_HOSTS>({
table: "hosts",
});
useEffect(() => {
if (!open) {
setHostId(0);
}
}, [open]);
if (!additional_hosts) {
return null;
}
@@ -43,93 +38,15 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
return (
<Modal open={open} setOpen={setOpen} className="p-6">
<Stack className="gap-4 mb-6">
<H3 className="text-lg font-bold text-foreground-light dark:text-foreground-dark">
Add client
</H3>
<Span className="" variant="faded">
The client is added to your WireGuard network
</Span>
<Stack className="w-full items-stretch gap-6">
<ClientFormTitle />
<ClientFormHostSelect
hosts={hosts}
host_id={host_id}
setHostId={setHostId}
/>
<ClientFormAction host_id={host_id} />
</Stack>
<Form
submitHandler={(e, data) => {
submitClientForm({
data: data as AddClientFormData,
busy,
open,
setBusy,
setError,
setOpen,
error,
onCreated,
host_id,
});
}}
>
<Stack className="w-full items-stretch gap-6">
<Select
options={hosts.map((h) => ({
value: (h.id || 0).toString(),
title: h.id == 0 ? `Main Host` : `Host ${h.id}`,
}))}
changeHandler={(v) => {
const selected_host = hosts.find(
(h) => h.id == Number(v),
);
if (typeof selected_host?.id == "number") {
set_host_id(selected_host.id);
}
}}
/>
<Input
name="client_name"
id="client_name"
placeholder="e.g. MacBook Pro"
label="Client name"
autoFocus
showLabel
required
/>
<Row className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
name="tunnel_ip"
id="tunnel_ip"
placeholder="10.0.0.10"
label="Tunnel IP"
showLabel
/>
<Input
name="allowed_ips"
id="allowed_ips"
placeholder="10.0.0.10/24"
label="Allowed IPs"
showLabel
/>
</Row>
{error ? (
<Span className="text-[12.5px] text-error dark:text-error">
{error}
</Span>
) : null}
<Row className="justify-end gap-2 mt-2">
<AdminButton
onClick={() => setOpen(false)}
disabled={busy}
>
Cancel
</AdminButton>
<AdminButton
variant="primary"
type="submit"
disabled={busy}
>
{busy ? "Adding…" : "Add client"}
</AdminButton>
</Row>
</Stack>
</Form>
</Modal>
);
}
}
@@ -0,0 +1,17 @@
import H3 from "@/src/components/twui/layout/H3";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
export default function ClientFormTitle() {
return (
<Stack className="gap-2 mb-6">
<H3 className="text-lg font-bold text-foreground-light dark:text-foreground-dark">
Select Host
</H3>
<Span variant="faded">
Choose the host the client will be added to, then continue on
its add-client form
</Span>
</Stack>
);
}
@@ -28,7 +28,7 @@ const thRightClass = `${thClass} text-right`;
export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
const [query, setQuery] = useState("");
const { res, setRes } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
const { res } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
table: "clients",
sql_query: {
order: { field: "created_at", strategy: "DESC" },
@@ -103,10 +103,11 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
</Stack>
) : filtered.length ? (
<div className="overflow-x-auto">
<table className="w-full min-w-[860px]">
<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>
@@ -144,11 +145,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
</Span>
</Stack>
)}
<ClientFormModal
open={addOpen}
setOpen={setAddOpen}
onCreated={() => setRes(undefined)}
/>
<ClientFormModal open={addOpen} setOpen={setAddOpen} />
</AdminCard>
);
}
@@ -17,6 +17,7 @@ export default function AddClientFormWgIpAddress({
form,
setForm,
host_id,
existing_full,
}: Props) {
const [grabbing, setGrabbing] = useState(false);
const [grab_error, setGrabError] = useState<string>();
@@ -52,23 +53,26 @@ export default function AddClientFormWgIpAddress({
}));
}}
defaultValue={form.wg_ip_address}
readOnly={Boolean(existing_full?.id)}
showLabel
/>
<Button
title="Grab the next available client IP from the host's WireGuard subnet"
variant="outlined"
color="primary"
size="small"
beforeIcon={<Wand2 size={16} />}
loading={grabbing}
onClick={(e) => {
e.preventDefault();
handleGrabNextIp();
}}
>
Grab Next Available IP
</Button>
{existing_full?.id ? null : (
<Button
title="Grab the next available client IP from the host's WireGuard subnet"
variant="outlined"
color="primary"
size="small"
beforeIcon={<Wand2 size={16} />}
loading={grabbing}
onClick={(e) => {
e.preventDefault();
handleGrabNextIp();
}}
>
Grab Next Available IP
</Button>
)}
</Row>
{grab_error ? (
+2 -2
View File
@@ -7,11 +7,11 @@ export default function formatUnixTimestamp({ timestamp }: Params) {
return "—";
}
const date = new Date(timestamp * 1000);
const date = new Date(Number(timestamp));
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
}