Refactor add new host form
This commit is contained in:
@@ -35,7 +35,10 @@ export default function GrabNextSubnetButton({ onGrab }: Props) {
|
||||
color="primary"
|
||||
beforeIcon={<Wand2 size={16} />}
|
||||
loading={grabbing}
|
||||
onClick={grabNextSubnet}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
grabNextSubnet();
|
||||
}}
|
||||
>
|
||||
Grab Next Subnet
|
||||
</Button>
|
||||
|
||||
@@ -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<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
|
||||
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<AvailabilityType>();
|
||||
|
||||
@@ -82,6 +86,7 @@ export default function HostWgIpField({
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-2">
|
||||
<Row className="w-full flex-nowrap">
|
||||
<Input
|
||||
value={value}
|
||||
changeHandler={(v) => {
|
||||
@@ -97,6 +102,16 @@ export default function HostWgIpField({
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
|
||||
<GrabNextSubnetButton
|
||||
onGrab={(ip) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
wg_ip_address: ip,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{ip_status === "invalid" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
|
||||
@@ -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 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<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 (
|
||||
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<AddHostForm />
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user