Updates
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { useState } from "react";
|
||||
import { Wand2 } from "lucide-react";
|
||||
import Button from "../twui/layout/Button";
|
||||
import fetchApi from "../twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
onGrab?: (ip: string) => void;
|
||||
};
|
||||
|
||||
export default function GrabNextSubnetButton({ onGrab }: Props) {
|
||||
const [grabbing, setGrabbing] = useState(false);
|
||||
|
||||
function grabNextSubnet() {
|
||||
setGrabbing(true);
|
||||
|
||||
fetchApi<ApiReqParams, { success: boolean; msg?: string }>(
|
||||
`/api/admin/grab-next-available-private-ip`,
|
||||
{ method: "POST" },
|
||||
)
|
||||
.then((res) => {
|
||||
if (res?.success && res.msg) {
|
||||
onGrab?.(res.msg);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setGrabbing(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
title="Grab Next Available Subnet"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
beforeIcon={<Wand2 size={16} />}
|
||||
loading={grabbing}
|
||||
onClick={grabNextSubnet}
|
||||
>
|
||||
Grab Next Subnet
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useEffect, useState, type RefObject } from "react";
|
||||
import { CircleCheck, Loader2, Network, TriangleAlert } from "lucide-react";
|
||||
import Input from "../twui/form/Input";
|
||||
import Row from "../twui/layout/Row";
|
||||
import Span from "../twui/layout/Span";
|
||||
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";
|
||||
|
||||
export type HostWgIpFieldStatus =
|
||||
| "checking"
|
||||
| "available"
|
||||
| "not_available"
|
||||
| "invalid"
|
||||
| "current";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
onStatus?: (status: HostWgIpFieldStatus) => void;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
current_value?: string;
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
|
||||
const subnet_ip_pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.1$/;
|
||||
|
||||
type AvailabilityType = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export default function HostWgIpField({
|
||||
value,
|
||||
onChange,
|
||||
onStatus,
|
||||
inputRef,
|
||||
current_value,
|
||||
autoFocus,
|
||||
}: Props) {
|
||||
const [availability, setAvailability] = useState<AvailabilityType>();
|
||||
|
||||
useEffect(() => {
|
||||
setAvailability(undefined);
|
||||
|
||||
if (!subnet_ip_pattern.test(value)) return;
|
||||
if (current_value && value == current_value) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
fetchApi<ApiReqParams, AvailabilityType>(
|
||||
`/api/admin/check-private-ip-address-availability`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { ip_address: value },
|
||||
},
|
||||
).then((res) => {
|
||||
if (cancelled) return;
|
||||
setAvailability(res);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [value, current_value]);
|
||||
|
||||
const is_current = Boolean(current_value && value == current_value);
|
||||
|
||||
const ip_status: HostWgIpFieldStatus = is_current
|
||||
? "current"
|
||||
: subnet_ip_pattern.test(value)
|
||||
? availability
|
||||
? availability.success
|
||||
? "available"
|
||||
: "not_available"
|
||||
: "checking"
|
||||
: "invalid";
|
||||
|
||||
useEffect(() => {
|
||||
onStatus?.(ip_status);
|
||||
}, [ip_status, onStatus]);
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-2">
|
||||
<Input
|
||||
defaultValue={value}
|
||||
changeHandler={(v) => {
|
||||
onChange?.(v);
|
||||
}}
|
||||
prefix={<Network size={17} opacity={0.5} />}
|
||||
suffix={
|
||||
<Span className="text-sm opacity-50 whitespace-nowrap">
|
||||
WireGuard IP Address
|
||||
</Span>
|
||||
}
|
||||
componentRef={inputRef}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
|
||||
{ip_status === "invalid" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="error"
|
||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||
>
|
||||
<Row>
|
||||
<TriangleAlert size={15} />
|
||||
<Span>Invalid IP</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : ip_status === "current" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
||||
>
|
||||
<Row>
|
||||
<CircleCheck size={15} />
|
||||
<Span>Current value</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : ip_status === "not_available" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="error"
|
||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||
>
|
||||
<Row>
|
||||
<TriangleAlert size={15} />
|
||||
<Span>Not available</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : ip_status === "available" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="success"
|
||||
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
||||
>
|
||||
<Row>
|
||||
<CircleCheck size={15} />
|
||||
<Span>Available</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
||||
>
|
||||
<Row>
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
<Span>Checking availability…</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,164 +1,41 @@
|
||||
import { useEffect, useRef, useState, type ComponentProps } from "react";
|
||||
import { useState, type ComponentProps } from "react";
|
||||
import Button from "../twui/layout/Button";
|
||||
import useStatus from "../twui/hooks/useStatus";
|
||||
import fetchApi from "../twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import Row from "../twui/layout/Row";
|
||||
import Input from "../twui/form/Input";
|
||||
import Stack from "../twui/layout/Stack";
|
||||
import {
|
||||
CircleCheck,
|
||||
Loader2,
|
||||
Network,
|
||||
TriangleAlert,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import Span from "../twui/layout/Span";
|
||||
import Tag from "../twui/elements/Tag";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import HostWgIpField, {
|
||||
type HostWgIpFieldStatus,
|
||||
} from "./host-wg-ip-field";
|
||||
import GrabNextSubnetButton from "./grab-next-subnet-button";
|
||||
|
||||
type Props = {
|
||||
button_props?: Omit<ComponentProps<typeof Button>, "title">;
|
||||
};
|
||||
|
||||
type AvailabilityType = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
type GrabSubnetType = {
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
};
|
||||
|
||||
const subnet_ip_pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.1$/;
|
||||
|
||||
type IPStatusType = "checking" | "available" | "not_available" | "invalid";
|
||||
|
||||
export default function SetupMainHostButton({ button_props }: Props) {
|
||||
const { loading, setLoading, status, setStatus } = useStatus();
|
||||
|
||||
const [wgIP, setWgIP] = useState<string>(AppData["DefaultPrivateIP"]);
|
||||
const [availability, setAvailability] = useState<AvailabilityType>();
|
||||
const [grabbing, setGrabbing] = useState(false);
|
||||
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailability(undefined);
|
||||
|
||||
if (!subnet_ip_pattern.test(wgIP)) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
fetchApi<ApiReqParams, AvailabilityType>(
|
||||
`/api/admin/check-private-ip-address-availability`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { ip_address: wgIP },
|
||||
},
|
||||
).then((res) => {
|
||||
if (cancelled) return;
|
||||
setAvailability(res);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [wgIP]);
|
||||
|
||||
const ip_status: IPStatusType = subnet_ip_pattern.test(wgIP)
|
||||
? availability
|
||||
? availability.success
|
||||
? "available"
|
||||
: "not_available"
|
||||
: "checking"
|
||||
: "invalid";
|
||||
|
||||
function grabNextSubnet() {
|
||||
setGrabbing(true);
|
||||
|
||||
fetchApi<ApiReqParams, GrabSubnetType>(
|
||||
`/api/admin/grab-next-available-private-ip`,
|
||||
{ method: "POST" },
|
||||
)
|
||||
.then((res) => {
|
||||
if (res?.success && res.msg) {
|
||||
setWgIP(res.msg);
|
||||
if (input_ref.current) {
|
||||
input_ref.current.value = res.msg;
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setGrabbing(false);
|
||||
});
|
||||
}
|
||||
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||
|
||||
return (
|
||||
<Stack className="w-full items-stretch gap-3">
|
||||
<Stack className="w-full gap-2">
|
||||
<Input
|
||||
defaultValue={wgIP}
|
||||
changeHandler={(v) => {
|
||||
setWgIP(v);
|
||||
}}
|
||||
prefix={<Network size={17} opacity={0.5} />}
|
||||
suffix={
|
||||
<Span className="text-sm opacity-50 whitespace-nowrap">
|
||||
Selected Wireguard IP Address
|
||||
</Span>
|
||||
}
|
||||
componentRef={input_ref}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{ip_status === "invalid" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="error"
|
||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||
>
|
||||
<Row>
|
||||
<TriangleAlert size={15} />
|
||||
<span>Invalid IP</span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : ip_status === "not_available" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="error"
|
||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||
>
|
||||
<Row>
|
||||
<TriangleAlert size={15} />
|
||||
<span>Not available</span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : ip_status === "available" ? (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="success"
|
||||
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
||||
>
|
||||
<Row>
|
||||
<CircleCheck size={15} />
|
||||
<span>Available</span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
||||
>
|
||||
<Row>
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
<span>Checking availability…</span>
|
||||
</Row>
|
||||
</Tag>
|
||||
)}
|
||||
</Stack>
|
||||
<HostWgIpField
|
||||
value={wgIP}
|
||||
onChange={setWgIP}
|
||||
onStatus={setIpStatus}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{status?.error && status.msg ? (
|
||||
<Tag
|
||||
@@ -168,7 +45,7 @@ export default function SetupMainHostButton({ button_props }: Props) {
|
||||
>
|
||||
<Row>
|
||||
<TriangleAlert size={15} />
|
||||
<span>{status.msg}</span>
|
||||
<Span>{status.msg}</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : null}
|
||||
@@ -180,27 +57,18 @@ export default function SetupMainHostButton({ button_props }: Props) {
|
||||
>
|
||||
<Row>
|
||||
<CircleCheck size={15} />
|
||||
<span>{status.msg}</span>
|
||||
<Span>{status.msg}</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
) : null}
|
||||
|
||||
<Row className="w-full items-center gap-2">
|
||||
<Button
|
||||
title="Grab Next Available Subnet"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
beforeIcon={<Wand2 size={16} />}
|
||||
loading={grabbing}
|
||||
onClick={grabNextSubnet}
|
||||
>
|
||||
Grab Next Subnet
|
||||
</Button>
|
||||
<GrabNextSubnetButton onGrab={setWgIP} />
|
||||
|
||||
<Button
|
||||
title="Setup Main Host"
|
||||
{...button_props}
|
||||
disabled={ip_status !== "available"}
|
||||
disabled={ipStatus !== "available"}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
@@ -234,4 +102,4 @@ export default function SetupMainHostButton({ button_props }: Props) {
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user