diff --git a/src/components/general/grab-next-subnet-button.tsx b/src/components/general/grab-next-subnet-button.tsx index c91b7d9..94a82e3 100644 --- a/src/components/general/grab-next-subnet-button.tsx +++ b/src/components/general/grab-next-subnet-button.tsx @@ -35,9 +35,12 @@ export default function GrabNextSubnetButton({ onGrab }: Props) { color="primary" beforeIcon={} loading={grabbing} - onClick={grabNextSubnet} + onClick={(e) => { + e.preventDefault(); + grabNextSubnet(); + }} > Grab Next Subnet ); -} \ No newline at end of file +} diff --git a/src/components/general/host-wg-ip-field.tsx b/src/components/general/host-wg-ip-field.tsx index 5af6111..ef59542 100644 --- a/src/components/general/host-wg-ip-field.tsx +++ b/src/components/general/host-wg-ip-field.tsx @@ -7,6 +7,9 @@ import Tag from "../twui/elements/Tag"; import Stack from "../twui/layout/Stack"; import fetchApi from "../twui/utils/fetch/fetchApi"; 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 = | "checking" @@ -15,7 +18,7 @@ export type HostWgIpFieldStatus = | "invalid" | "current"; -type Props = { +type Props = ReturnType> & { value: string; onChange?: (value: string) => void; onStatus?: (status: HostWgIpFieldStatus) => void; @@ -37,6 +40,7 @@ export default function HostWgIpField({ inputRef, current_value, autoFocus, + setForm, }: Props) { const [availability, setAvailability] = useState(); @@ -82,20 +86,31 @@ export default function HostWgIpField({ return ( - { - onChange?.(v); - }} - prefix={} - suffix={ - - WireGuard IP Address - - } - componentRef={inputRef} - autoFocus={autoFocus} - /> + + { + onChange?.(v); + }} + prefix={} + suffix={ + + WireGuard IP Address + + } + componentRef={inputRef} + autoFocus={autoFocus} + /> + + { + setForm((prev) => ({ + ...prev, + wg_ip_address: ip, + })); + }} + /> + {ip_status === "invalid" ? ( >) { + try { + if (!window.confirm("Create this new host?")) { + return; + } + + setLoading(true); + + const res = await fetchApi( + `/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); + } +} \ No newline at end of file diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-action.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-action.tsx new file mode 100644 index 0000000..5b910a9 --- /dev/null +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-action.tsx @@ -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> & { + ipStatus: HostWgIpFieldStatus; +}; + +export default function AddHostFormAction({ + status, + ipStatus, +}: Props) { + if (status?.error) { + return null; + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-subnet.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-subnet.tsx new file mode 100644 index 0000000..9ae1d38 --- /dev/null +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-subnet.tsx @@ -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 ( + +

+ WireGuard subnet +

+

+ 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. +

+
+ ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-wg-ip-address.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-wg-ip-address.tsx new file mode 100644 index 0000000..7469595 --- /dev/null +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-wg-ip-address.tsx @@ -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> & { + setIpStatus: (status: HostWgIpFieldStatus) => void; +}; + +export default function AddHostFormWgIpAddress({ + setIpStatus, + ...props +}: Props) { + return ( + + { + props.setForm((prev) => ({ + ...prev, + wg_ip_address: v, + })); + }} + onStatus={setIpStatus} + autoFocus + {...props} + /> + + ); +} diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx new file mode 100644 index 0000000..ecf39fe --- /dev/null +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx @@ -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({ + default: { + wg_ip_address: AppData["DefaultPrivateIP"], + }, + title: "Add Host", + }); + + const [ipStatus, setIpStatus] = useState("checking"); + + const { loading, status } = init; + + return ( +
{ + submitAddHostForm(init); + }} + > + {status?.error && {status.msg}} + {loading && } + {init.ready ? ( + + + + + + ) : ( + + )} + + ); +} diff --git a/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx b/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx index a90e60e..6ae08e8 100644 --- a/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx +++ b/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx @@ -1,126 +1,10 @@ -import { useState } from "react"; -import { CircleCheck, TriangleAlert } from "lucide-react"; import AdminCard from "@/src/components/general/admin-card"; -import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-button"; -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"; +import AddHostForm from "../(partials)/add-host-form/add-host-form"; export default function AddHostFormSection() { - const { loading, setLoading, status, setStatus } = useStatus(); - - const [wgIP, setWgIP] = useState(AppData["DefaultPrivateIP"]); - const [ipStatus, setIpStatus] = useState("checking"); - - function handleCreate() { - if (!window.confirm("Create this new host?")) return; - - setStatus(undefined); - setLoading(true); - - fetchApi( - `/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 ( - -

- WireGuard subnet -

-

- 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. -

-
- - - - {status?.error && status.msg ? ( - - - - {status.msg} - - - ) : null} - {status?.success && status.msg ? ( - - - - {status.msg} - - - ) : null} - - - - - - +
); } \ No newline at end of file