Refactor add new host form
This commit is contained in:
@@ -35,9 +35,12 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user