Refactor add new host form

This commit is contained in:
2026-09-20 11:59:50 +01:00
parent 25b64281f8
commit 957f2de13c
8 changed files with 212 additions and 135 deletions
@@ -35,7 +35,10 @@ export default function GrabNextSubnetButton({ onGrab }: Props) {
color="primary" color="primary"
beforeIcon={<Wand2 size={16} />} beforeIcon={<Wand2 size={16} />}
loading={grabbing} loading={grabbing}
onClick={grabNextSubnet} onClick={(e) => {
e.preventDefault();
grabNextSubnet();
}}
> >
Grab Next Subnet Grab Next Subnet
</Button> </Button>
+30 -15
View File
@@ -7,6 +7,9 @@ import Tag from "../twui/elements/Tag";
import Stack from "../twui/layout/Stack"; import Stack from "../twui/layout/Stack";
import fetchApi from "../twui/utils/fetch/fetchApi"; import fetchApi from "../twui/utils/fetch/fetchApi";
import type { ApiReqParams } from "@/src/types"; import type { ApiReqParams } from "@/src/types";
import GrabNextSubnetButton from "./grab-next-subnet-button";
import type useFormInit from "@/src/hooks/use-form-init";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
export type HostWgIpFieldStatus = export type HostWgIpFieldStatus =
| "checking" | "checking"
@@ -15,7 +18,7 @@ export type HostWgIpFieldStatus =
| "invalid" | "invalid"
| "current"; | "current";
type Props = { type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
value: string; value: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
onStatus?: (status: HostWgIpFieldStatus) => void; onStatus?: (status: HostWgIpFieldStatus) => void;
@@ -37,6 +40,7 @@ export default function HostWgIpField({
inputRef, inputRef,
current_value, current_value,
autoFocus, autoFocus,
setForm,
}: Props) { }: Props) {
const [availability, setAvailability] = useState<AvailabilityType>(); const [availability, setAvailability] = useState<AvailabilityType>();
@@ -82,20 +86,31 @@ export default function HostWgIpField({
return ( return (
<Stack className="w-full gap-2"> <Stack className="w-full gap-2">
<Input <Row className="w-full flex-nowrap">
value={value} <Input
changeHandler={(v) => { value={value}
onChange?.(v); changeHandler={(v) => {
}} onChange?.(v);
prefix={<Network size={17} opacity={0.5} />} }}
suffix={ prefix={<Network size={17} opacity={0.5} />}
<Span className="text-sm opacity-50 whitespace-nowrap"> suffix={
WireGuard IP Address <Span className="text-sm opacity-50 whitespace-nowrap">
</Span> WireGuard IP Address
} </Span>
componentRef={inputRef} }
autoFocus={autoFocus} componentRef={inputRef}
/> autoFocus={autoFocus}
/>
<GrabNextSubnetButton
onGrab={(ip) => {
setForm((prev) => ({
...prev,
wg_ip_address: ip,
}));
}}
/>
</Row>
{ip_status === "invalid" ? ( {ip_status === "invalid" ? (
<Tag <Tag
@@ -0,0 +1,44 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
import type useFormInit from "@/src/hooks/use-form-init";
import type { ApiReqParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
export default async function submitAddHostForm({
setLoading,
setStatus,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>) {
try {
if (!window.confirm("Create this new host?")) {
return;
}
setLoading(true);
const res = await fetchApi<ApiReqParams, APIResponseObject>(
`/api/admin/add-host`,
{
method: "POST",
body: {
wg_ip_address: form.wg_ip_address || "",
},
},
);
if (res.success && res.numberRes) {
window.location.pathname = `/admin/hosts/${res.numberRes}`;
return;
}
throw new Error(res.msg || "Could not create the host");
} catch (error: any) {
setStatus({
error: true,
success: false,
msg: error.message || "Could not create the host",
});
setLoading(false);
}
}
@@ -0,0 +1,27 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Button from "@/src/components/twui/layout/Button";
import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field";
import useFormInit from "@/src/hooks/use-form-init";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
ipStatus: HostWgIpFieldStatus;
};
export default function AddHostFormAction({
status,
ipStatus,
}: Props) {
if (status?.error) {
return null;
}
return (
<Button
title="Create Host"
type="submit"
disabled={ipStatus !== "available"}
>
Create Host
</Button>
);
}
@@ -0,0 +1,20 @@
import H3 from "@/src/components/twui/layout/H3";
import P from "@/src/components/twui/layout/P";
import Stack from "@/src/components/twui/layout/Stack";
export default function AddHostFormSubnet() {
return (
<Stack className="gap-1">
<H3 className="text-[14px] font-semibold mb-0!">
WireGuard subnet
</H3>
<P
noMargin
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
>
Each host runs on its own private subnet (e.g. 10.2.0.1/24).
The tunnel is brought up and restarted as part of creation.
</P>
</Stack>
);
}
@@ -0,0 +1,33 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-button";
import HostWgIpField, {
type HostWgIpFieldStatus,
} from "@/src/components/general/host-wg-ip-field";
import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
setIpStatus: (status: HostWgIpFieldStatus) => void;
};
export default function AddHostFormWgIpAddress({
setIpStatus,
...props
}: Props) {
return (
<Stack className="w-full gap-2">
<HostWgIpField
value={props.form.wg_ip_address || ""}
onChange={(v) => {
props.setForm((prev) => ({
...prev,
wg_ip_address: v,
}));
}}
onStatus={setIpStatus}
autoFocus
{...props}
/>
</Stack>
);
}
@@ -0,0 +1,51 @@
import { useState } from "react";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Form from "@/src/components/twui/form/Form";
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
import Stack from "@/src/components/twui/layout/Stack";
import Tag from "@/src/components/twui/elements/Tag";
import useFormInit from "@/src/hooks/use-form-init";
import { AppData } from "@/src/data/app-data";
import submitAddHostForm from "../../(functions)/submit-add-host-form";
import AddHostFormAction from "./add-host-form-action";
import AddHostFormSubnet from "./add-host-form-subnet";
import AddHostFormWgIpAddress from "./add-host-form-wg-ip-address";
import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock";
import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field";
export default function AddHostForm() {
const init = useFormInit<BUN_SQLITE_WGUI_HOSTS>({
default: {
wg_ip_address: AppData["DefaultPrivateIP"],
},
title: "Add Host",
});
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
const { loading, status } = init;
return (
<Form
className="w-full gap-6"
onSubmit={() => {
submitAddHostForm(init);
}}
>
{status?.error && <Tag color="error">{status.msg}</Tag>}
{loading && <LoadingOverlay />}
{init.ready ? (
<Stack className="w-full gap-2">
<AddHostFormSubnet />
<AddHostFormWgIpAddress
{...init}
setIpStatus={setIpStatus}
/>
<AddHostFormAction {...init} ipStatus={ipStatus} />
</Stack>
) : (
<LoadingRectangleBlock className="h-[300px]" />
)}
</Form>
);
}
@@ -1,126 +1,10 @@
import { useState } from "react";
import { CircleCheck, TriangleAlert } from "lucide-react";
import AdminCard from "@/src/components/general/admin-card"; import AdminCard from "@/src/components/general/admin-card";
import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-button"; import AddHostForm from "../(partials)/add-host-form/add-host-form";
import HostWgIpField, {
type HostWgIpFieldStatus,
} from "@/src/components/general/host-wg-ip-field";
import useStatus from "@/src/components/twui/hooks/useStatus";
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
import H3 from "@/src/components/twui/layout/H3";
import P from "@/src/components/twui/layout/P";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
import Tag from "@/src/components/twui/elements/Tag";
import Button from "@/src/components/twui/layout/Button";
import { AppData } from "@/src/data/app-data";
import type { ApiReqParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
export default function AddHostFormSection() { export default function AddHostFormSection() {
const { loading, setLoading, status, setStatus } = useStatus();
const [wgIP, setWgIP] = useState<string>(AppData["DefaultPrivateIP"]);
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
function handleCreate() {
if (!window.confirm("Create this new host?")) return;
setStatus(undefined);
setLoading(true);
fetchApi<ApiReqParams, APIResponseObject>(
`/api/admin/add-host`,
{
method: "POST",
body: { wg_ip_address: wgIP },
},
)
.then((res) => {
if (res.success && res.numberRes) {
window.location.pathname = `/admin/hosts/${res.numberRes}`;
return;
}
setStatus({
error: true,
success: false,
msg: res.msg || "Could not create the host",
});
})
.catch((error: any) => {
setStatus({
error: true,
success: false,
msg: error.message || "Could not create the host",
});
})
.finally(() => {
setLoading(false);
});
}
return ( return (
<AdminCard className="w-full p-5 flex flex-col gap-4"> <AdminCard className="w-full p-5 flex flex-col gap-4">
<Stack className="gap-1"> <AddHostForm />
<H3 className="text-[14px] font-semibold mb-0!">
WireGuard subnet
</H3>
<P
noMargin
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
>
Each host runs on its own private subnet (e.g.
10.2.0.1/24). The tunnel is brought up and restarted as
part of creation.
</P>
</Stack>
<HostWgIpField
value={wgIP}
onChange={setWgIP}
onStatus={setIpStatus}
autoFocus
/>
{status?.error && status.msg ? (
<Tag
variant="outlined"
color="error"
className="w-full py-1.5 outline-error/50 bg-error/5!"
>
<Row>
<TriangleAlert size={15} />
<Span>{status.msg}</Span>
</Row>
</Tag>
) : null}
{status?.success && status.msg ? (
<Tag
variant="outlined"
color="success"
className="w-full py-1.5 outline-success/50 bg-success/5!"
>
<Row>
<CircleCheck size={15} />
<Span>{status.msg}</Span>
</Row>
</Tag>
) : null}
<Row className="w-full items-center gap-2">
<GrabNextSubnetButton onGrab={setWgIP} />
<Button
title="Create Host"
disabled={ipStatus !== "available"}
loading={loading}
onClick={handleCreate}
>
Create Host
</Button>
</Row>
</AdminCard> </AdminCard>
); );
} }