Updates
This commit is contained in:
@@ -58,6 +58,20 @@ export default function ClientRow({ client, rules }: Props) {
|
|||||||
{client.name || "—"}
|
{client.name || "—"}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</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">
|
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/60 dark:text-foreground-dark/60 whitespace-nowrap">
|
||||||
{client.wg_ip_address || "—"}
|
{client.wg_ip_address || "—"}
|
||||||
</td>
|
</td>
|
||||||
@@ -86,7 +100,7 @@ export default function ClientRow({ client, rules }: Props) {
|
|||||||
{formatUnixTimestamp({ timestamp: client.created_at })}
|
{formatUnixTimestamp({ timestamp: client.created_at })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-[9px] text-right">
|
<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
|
<Link
|
||||||
href={`/admin/hosts/${host_id}/clients/${client.id}/edit`}
|
href={`/admin/hosts/${host_id}/clients/${client.id}/edit`}
|
||||||
title={`Edit client ${client.name || `#${client.id}`}`}
|
title={`Edit client ${client.name || `#${client.id}`}`}
|
||||||
|
|||||||
@@ -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 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 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 type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
import Select from "@/src/components/twui/form/Select";
|
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||||
import { Globe } from "lucide-react";
|
import ClientFormTitle from "./client-form-title";
|
||||||
|
import ClientFormHostSelect from "./client-form-host-select";
|
||||||
|
import ClientFormAction from "./client-form-action";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
onCreated?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
export default function ClientFormModal({ open, setOpen }: Props) {
|
||||||
const [busy, setBusy] = useState(false);
|
const [host_id, setHostId] = useState(0);
|
||||||
const [error, setError] = useState<string>();
|
|
||||||
|
|
||||||
const [host_id, set_host_id] = useState(0);
|
|
||||||
|
|
||||||
const { res: additional_hosts } = useAdminCrudGet<BUN_SQLITE_WGUI_HOSTS>({
|
const { res: additional_hosts } = useAdminCrudGet<BUN_SQLITE_WGUI_HOSTS>({
|
||||||
table: "hosts",
|
table: "hosts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setHostId(0);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
if (!additional_hosts) {
|
if (!additional_hosts) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -43,93 +38,15 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} setOpen={setOpen} className="p-6">
|
<Modal open={open} setOpen={setOpen} className="p-6">
|
||||||
<Stack className="gap-4 mb-6">
|
<Stack className="w-full items-stretch gap-6">
|
||||||
<H3 className="text-lg font-bold text-foreground-light dark:text-foreground-dark">
|
<ClientFormTitle />
|
||||||
Add client
|
<ClientFormHostSelect
|
||||||
</H3>
|
hosts={hosts}
|
||||||
<Span className="" variant="faded">
|
host_id={host_id}
|
||||||
The client is added to your WireGuard network
|
setHostId={setHostId}
|
||||||
</Span>
|
/>
|
||||||
|
<ClientFormAction host_id={host_id} />
|
||||||
</Stack>
|
</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>
|
</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) {
|
export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const { res, setRes } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
|
const { res } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
|
||||||
table: "clients",
|
table: "clients",
|
||||||
sql_query: {
|
sql_query: {
|
||||||
order: { field: "created_at", strategy: "DESC" },
|
order: { field: "created_at", strategy: "DESC" },
|
||||||
@@ -103,10 +103,11 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
) : filtered.length ? (
|
) : filtered.length ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[860px]">
|
<table className="w-full min-w-[940px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
|
<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}>Client</th>
|
||||||
|
<th className={thClass}>Host</th>
|
||||||
<th className={thClass}>Tunnel IP</th>
|
<th className={thClass}>Tunnel IP</th>
|
||||||
<th className={thClass}>Allowed IPs</th>
|
<th className={thClass}>Allowed IPs</th>
|
||||||
<th className={thClass}>Access</th>
|
<th className={thClass}>Access</th>
|
||||||
@@ -144,11 +145,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
|||||||
</Span>
|
</Span>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<ClientFormModal
|
<ClientFormModal open={addOpen} setOpen={setAddOpen} />
|
||||||
open={addOpen}
|
|
||||||
setOpen={setAddOpen}
|
|
||||||
onCreated={() => setRes(undefined)}
|
|
||||||
/>
|
|
||||||
</AdminCard>
|
</AdminCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+18
-14
@@ -17,6 +17,7 @@ export default function AddClientFormWgIpAddress({
|
|||||||
form,
|
form,
|
||||||
setForm,
|
setForm,
|
||||||
host_id,
|
host_id,
|
||||||
|
existing_full,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [grabbing, setGrabbing] = useState(false);
|
const [grabbing, setGrabbing] = useState(false);
|
||||||
const [grab_error, setGrabError] = useState<string>();
|
const [grab_error, setGrabError] = useState<string>();
|
||||||
@@ -52,23 +53,26 @@ export default function AddClientFormWgIpAddress({
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
defaultValue={form.wg_ip_address}
|
defaultValue={form.wg_ip_address}
|
||||||
|
readOnly={Boolean(existing_full?.id)}
|
||||||
showLabel
|
showLabel
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
{existing_full?.id ? null : (
|
||||||
title="Grab the next available client IP from the host's WireGuard subnet"
|
<Button
|
||||||
variant="outlined"
|
title="Grab the next available client IP from the host's WireGuard subnet"
|
||||||
color="primary"
|
variant="outlined"
|
||||||
size="small"
|
color="primary"
|
||||||
beforeIcon={<Wand2 size={16} />}
|
size="small"
|
||||||
loading={grabbing}
|
beforeIcon={<Wand2 size={16} />}
|
||||||
onClick={(e) => {
|
loading={grabbing}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
handleGrabNextIp();
|
e.preventDefault();
|
||||||
}}
|
handleGrabNextIp();
|
||||||
>
|
}}
|
||||||
Grab Next Available IP
|
>
|
||||||
</Button>
|
Grab Next Available IP
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{grab_error ? (
|
{grab_error ? (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function formatUnixTimestamp({ timestamp }: Params) {
|
|||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(Number(timestamp));
|
||||||
|
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|||||||
Reference in New Issue
Block a user