mirror of https://github.com/jetkvm/kvm.git
Compare commits
No commits in common. "750d21ad8139e6a4a909e125b0145fa59457b739" and "9d0e62f80ee83794b61bd389413eef5c49af86c5" have entirely different histories.
750d21ad81
...
9d0e62f80e
|
|
@ -1,10 +1,13 @@
|
||||||
import { CheckCircleIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
import { CloseButton } from "@headlessui/react";
|
CheckCircleIcon,
|
||||||
import { LuInfo, LuOctagonAlert, LuTriangleAlert } from "react-icons/lu";
|
ExclamationTriangleIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type Variant = "danger" | "success" | "warning" | "info";
|
type Variant = "danger" | "success" | "warning" | "info";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
|
|
@ -21,27 +24,27 @@ interface ConfirmDialogProps {
|
||||||
|
|
||||||
const variantConfig = {
|
const variantConfig = {
|
||||||
danger: {
|
danger: {
|
||||||
icon: LuOctagonAlert,
|
icon: ExclamationTriangleIcon,
|
||||||
iconClass: "text-red-600",
|
iconClass: "text-red-600",
|
||||||
iconBgClass: "bg-red-100 border border-red-500/90",
|
iconBgClass: "bg-red-100",
|
||||||
buttonTheme: "danger",
|
buttonTheme: "danger",
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircleIcon,
|
icon: CheckCircleIcon,
|
||||||
iconClass: "text-green-600",
|
iconClass: "text-green-600",
|
||||||
iconBgClass: "bg-green-100 border border-green-500/90",
|
iconBgClass: "bg-green-100",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: LuTriangleAlert,
|
icon: ExclamationTriangleIcon,
|
||||||
iconClass: "text-yellow-600",
|
iconClass: "text-yellow-600",
|
||||||
iconBgClass: "bg-yellow-100 border border-yellow-500/90",
|
iconBgClass: "bg-yellow-100",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "lightDanger",
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: LuInfo,
|
icon: InformationCircleIcon,
|
||||||
iconClass: "text-blue-600",
|
iconClass: "text-blue-600",
|
||||||
iconBgClass: "bg-blue-100 border border-blue-500/90",
|
iconBgClass: "bg-blue-100",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
} as Record<
|
} as Record<
|
||||||
|
|
@ -91,13 +94,12 @@ export function ConfirmDialog({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-x-2" autoFocus>
|
<div className="flex justify-end gap-x-2">
|
||||||
{cancelText && (
|
{cancelText && (
|
||||||
<CloseButton as={Button} size="SM" theme="blank" text={cancelText} />
|
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
type="button"
|
|
||||||
theme={buttonTheme}
|
theme={buttonTheme}
|
||||||
text={isConfirming ? `${confirmText}...` : confirmText}
|
text={isConfirming ? `${confirmText}...` : confirmText}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import { GridCard } from "@/components/Card";
|
||||||
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||||
import { NetworkState } from "@/hooks/stores";
|
import { NetworkState } from "@/hooks/stores";
|
||||||
|
|
||||||
import EmptyCard from "./EmptyCard";
|
|
||||||
|
|
||||||
export default function DhcpLeaseCard({
|
export default function DhcpLeaseCard({
|
||||||
networkState,
|
networkState,
|
||||||
setShowRenewLeaseConfirm,
|
setShowRenewLeaseConfirm,
|
||||||
|
|
@ -14,39 +12,14 @@ export default function DhcpLeaseCard({
|
||||||
networkState: NetworkState | null;
|
networkState: NetworkState | null;
|
||||||
setShowRenewLeaseConfirm: (show: boolean) => void;
|
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const isDhcpLeaseEmpty = Object.keys(networkState?.dhcp_lease || {}).length === 0;
|
|
||||||
|
|
||||||
if (isDhcpLeaseEmpty) {
|
|
||||||
return (
|
|
||||||
<EmptyCard
|
|
||||||
headline="No DHCP Lease information"
|
|
||||||
description="We haven't received any DHCP lease information from the device yet."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
DHCP Lease Information
|
DHCP Lease Information
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
size="XS"
|
|
||||||
theme="light"
|
|
||||||
type="button"
|
|
||||||
className="text-red-500"
|
|
||||||
text="Renew DHCP Lease"
|
|
||||||
LeadingIcon={LuRefreshCcw}
|
|
||||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-x-6 gap-y-2">
|
<div className="flex gap-x-6 gap-y-2">
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
{networkState?.dhcp_lease?.ip && (
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
|
@ -223,6 +196,17 @@ export default function DhcpLeaseCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
className="text-red-500"
|
||||||
|
text="Renew DHCP Lease"
|
||||||
|
LeadingIcon={LuRefreshCcw}
|
||||||
|
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { LuPlus, LuX } from "react-icons/lu";
|
import { LuPlus, LuX } from "react-icons/lu";
|
||||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import validator from "validator";
|
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
import { NetworkSettings } from "@/hooks/stores";
|
|
||||||
|
|
||||||
export default function StaticIpv4Card() {
|
export default function StaticIpv4Card() {
|
||||||
const formMethods = useFormContext<NetworkSettings>();
|
const formMethods = useFormContext();
|
||||||
const { register, formState, watch } = formMethods;
|
const { register, formState, watch } = formMethods;
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" });
|
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" });
|
||||||
|
|
@ -19,12 +17,6 @@ export default function StaticIpv4Card() {
|
||||||
}, [append, fields.length]);
|
}, [append, fields.length]);
|
||||||
|
|
||||||
const dns = watch("ipv4_static.dns");
|
const dns = watch("ipv4_static.dns");
|
||||||
|
|
||||||
const validate = (value: string) => {
|
|
||||||
if (!validator.isIP(value)) return "Invalid IP address";
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
|
@ -39,8 +31,7 @@ export default function StaticIpv4Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="192.168.1.100"
|
placeholder="192.168.1.100"
|
||||||
{...register("ipv4_static.address", { validate })}
|
{...register("ipv4_static.address")}
|
||||||
error={formState.errors.ipv4_static?.address?.message}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
|
|
@ -48,8 +39,7 @@ export default function StaticIpv4Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="255.255.255.0"
|
placeholder="255.255.255.0"
|
||||||
{...register("ipv4_static.netmask", { validate })}
|
{...register("ipv4_static.netmask")}
|
||||||
error={formState.errors.ipv4_static?.netmask?.message}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -58,8 +48,7 @@ export default function StaticIpv4Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="192.168.1.1"
|
placeholder="192.168.1.1"
|
||||||
{...register("ipv4_static.gateway", { validate })}
|
{...register("ipv4_static.gateway")}
|
||||||
error={formState.errors.ipv4_static?.gateway?.message}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* DNS server fields */}
|
{/* DNS server fields */}
|
||||||
|
|
@ -74,7 +63,13 @@ export default function StaticIpv4Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="1.1.1.1"
|
placeholder="1.1.1.1"
|
||||||
{...register(`ipv4_static.dns.${index}`, { validate })}
|
{...register(`ipv4_static.dns.${index}`, {
|
||||||
|
// validate: (value: string) => {
|
||||||
|
// if (value === "") return true;
|
||||||
|
// if (!validator.isIP(value)) return "Invalid IP address";
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
})}
|
||||||
error={formState.errors.ipv4_static?.dns?.[index]?.message}
|
error={formState.errors.ipv4_static?.dns?.[index]?.message}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@ import { useEffect } from "react";
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
import { NetworkSettings } from "@/hooks/stores";
|
|
||||||
|
|
||||||
export default function StaticIpv6Card() {
|
export default function StaticIpv6Card() {
|
||||||
const formMethods = useFormContext<NetworkSettings>();
|
const formMethods = useFormContext();
|
||||||
const { register, formState, watch } = formMethods;
|
const { register, formState, watch } = formMethods;
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({ name: "ipv6_static.dns" });
|
const { fields, append, remove } = useFieldArray({ name: "ipv6_static.dns" });
|
||||||
|
|
@ -19,29 +18,6 @@ export default function StaticIpv6Card() {
|
||||||
}, [append, fields.length]);
|
}, [append, fields.length]);
|
||||||
|
|
||||||
const dns = watch("ipv6_static.dns");
|
const dns = watch("ipv6_static.dns");
|
||||||
|
|
||||||
const cidrValidation = (value: string) => {
|
|
||||||
if (value === "") return true;
|
|
||||||
|
|
||||||
// Check if it's a valid IPv6 address with CIDR notation
|
|
||||||
const parts = value.split("/");
|
|
||||||
if (parts.length !== 2) return "Please use CIDR notation (e.g., 2001:db8::1/64)";
|
|
||||||
|
|
||||||
const [address, prefix] = parts;
|
|
||||||
if (!validator.isIP(address, 6)) return "Invalid IPv6 address";
|
|
||||||
const prefixNum = parseInt(prefix);
|
|
||||||
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
|
|
||||||
return "Prefix must be between 0 and 128";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ipv6Validation = (value: string) => {
|
|
||||||
if (!validator.isIP(value, 6)) return "Invalid IPv6 address";
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
|
@ -55,7 +31,22 @@ export default function StaticIpv6Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="2001:db8::1/64"
|
placeholder="2001:db8::1/64"
|
||||||
{...register("ipv6_static.address", { validate: cidrValidation })}
|
{...register("ipv6_static.address", {
|
||||||
|
validate: (value: string) => {
|
||||||
|
if (value === "") return true;
|
||||||
|
// Check if it's a valid IPv6 address with CIDR notation
|
||||||
|
const parts = value.split("/");
|
||||||
|
if (parts.length !== 2)
|
||||||
|
return "Please use CIDR notation (e.g., 2001:db8::1/64)";
|
||||||
|
const [address, prefix] = parts;
|
||||||
|
if (!validator.isIP(address, 6)) return "Invalid IPv6 address";
|
||||||
|
const prefixNum = parseInt(prefix);
|
||||||
|
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
|
||||||
|
return "Prefix must be between 0 and 128";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
error={formState.errors.ipv6_static?.address?.message}
|
error={formState.errors.ipv6_static?.address?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -64,8 +55,7 @@ export default function StaticIpv6Card() {
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="2001:db8::1"
|
placeholder="2001:db8::1"
|
||||||
{...register("ipv6_static.gateway", { validate: ipv6Validation })}
|
{...register("ipv6_static.gateway")}
|
||||||
error={formState.errors.ipv6_static?.gateway?.message}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* DNS server fields */}
|
{/* DNS server fields */}
|
||||||
|
|
@ -81,7 +71,11 @@ export default function StaticIpv6Card() {
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="2001:4860:4860::8888"
|
placeholder="2001:4860:4860::8888"
|
||||||
{...register(`ipv6_static.dns.${index}`, {
|
{...register(`ipv6_static.dns.${index}`, {
|
||||||
validate: ipv6Validation,
|
validate: (value: string) => {
|
||||||
|
if (value === "") return true;
|
||||||
|
if (!validator.isIP(value)) return "Invalid IP address";
|
||||||
|
return true;
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
error={formState.errors.ipv6_static?.dns?.[index]?.message}
|
error={formState.errors.ipv6_static?.dns?.[index]?.message}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { LuEthernetPort } from "react-icons/lu";
|
import { LuEthernetPort } from "react-icons/lu";
|
||||||
|
|
@ -83,13 +83,6 @@ export default function SettingsNetworkRoute() {
|
||||||
|
|
||||||
// Confirm dialog
|
// Confirm dialog
|
||||||
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
||||||
const initialSettingsRef = useRef<NetworkSettings | null>(null);
|
|
||||||
|
|
||||||
const [showCriticalSettingsConfirm, setShowCriticalSettingsConfirm] = useState(false);
|
|
||||||
const [stagedSettings, setStagedSettings] = useState<NetworkSettings | null>(null);
|
|
||||||
const [criticalChanges, setCriticalChanges] = useState<
|
|
||||||
{ label: string; from: string; to: string }[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const fetchNetworkData = useCallback(async () => {
|
const fetchNetworkData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -123,7 +116,6 @@ export default function SettingsNetworkRoute() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
initialSettingsRef.current = settingsWithDefaults;
|
|
||||||
return { settings: settingsWithDefaults, state };
|
return { settings: settingsWithDefaults, state };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
|
@ -145,18 +137,28 @@ export default function SettingsNetworkRoute() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const prepareSettings = (data: FieldValues) => {
|
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
||||||
return {
|
|
||||||
|
const onSubmit = async (data: FieldValues) => {
|
||||||
|
const settings = {
|
||||||
...data,
|
...data,
|
||||||
|
|
||||||
// If custom domain option is selected, use the custom domain as value
|
// If custom domain option is selected, use the custom domain as value
|
||||||
domain: data.domain === "custom" ? customDomain : data.domain,
|
domain: data.domain === "custom" ? customDomain : data.domain,
|
||||||
} as NetworkSettings;
|
ipv4_static: {
|
||||||
|
...data.ipv4_static,
|
||||||
|
|
||||||
|
// Remove empty DNS entries
|
||||||
|
dns: data.ipv4_static?.dns.filter((dns: string) => dns.trim() !== ""),
|
||||||
|
},
|
||||||
|
ipv6_static: {
|
||||||
|
...data.ipv6_static,
|
||||||
|
|
||||||
|
// Remove empty DNS entries
|
||||||
|
dns: data.ipv6_static?.dns.filter((dns: string) => dns.trim() !== ""),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
|
||||||
|
|
||||||
const onSubmit = async (settings: NetworkSettings) => {
|
|
||||||
send("setNetworkSettings", { settings }, async resp => {
|
send("setNetworkSettings", { settings }, async resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
return notifications.error(
|
return notifications.error(
|
||||||
|
|
@ -172,54 +174,12 @@ export default function SettingsNetworkRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitGate = async (data: FieldValues) => {
|
|
||||||
const settings = prepareSettings(data);
|
|
||||||
const dirty = formState.dirtyFields;
|
|
||||||
|
|
||||||
// These fields will prompt a confirm dialog, all else save immediately
|
|
||||||
const criticalFields = [
|
|
||||||
// Label is for the UI, key is the internal key of the field
|
|
||||||
{ label: "IPv4 mode", key: "ipv4_mode" },
|
|
||||||
{ label: "IPv6 mode", key: "ipv6_mode" },
|
|
||||||
] as { label: string; key: keyof NetworkSettings }[];
|
|
||||||
|
|
||||||
const criticalChanged = criticalFields.some(field => dirty[field.key]);
|
|
||||||
|
|
||||||
// If no critical fields are changed, save immediately
|
|
||||||
if (!criticalChanged) return onSubmit(settings);
|
|
||||||
|
|
||||||
const changes = new Set<{ label: string; from: string; to: string }>();
|
|
||||||
criticalFields.forEach(field => {
|
|
||||||
const { key, label } = field;
|
|
||||||
if (dirty[key]) {
|
|
||||||
const from = initialSettingsRef?.current?.[key] as string;
|
|
||||||
const to = data[key] as string;
|
|
||||||
changes.add({ label, from, to });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setStagedSettings(settings);
|
|
||||||
setCriticalChanges(Array.from(changes));
|
|
||||||
setShowCriticalSettingsConfirm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ipv4mode = watch("ipv4_mode");
|
const ipv4mode = watch("ipv4_mode");
|
||||||
const ipv6mode = watch("ipv6_mode");
|
const ipv6mode = watch("ipv6_mode");
|
||||||
|
|
||||||
const onDhcpLeaseRenew = () => {
|
|
||||||
send("renewDHCPLease", {}, resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error("Failed to renew lease: " + resp.error.message);
|
|
||||||
} else {
|
|
||||||
notifications.success("DHCP lease renewed");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={handleSubmit(onSubmitGate)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Network"
|
title="Network"
|
||||||
description="Configure the network settings for the device"
|
description="Configure the network settings for the device"
|
||||||
|
|
@ -366,12 +326,6 @@ export default function SettingsNetworkRoute() {
|
||||||
</GridCard>
|
</GridCard>
|
||||||
) : ipv4mode === "static" ? (
|
) : ipv4mode === "static" ? (
|
||||||
<StaticIpv4Card />
|
<StaticIpv4Card />
|
||||||
) : ipv4mode === "dhcp" && !!formState.dirtyFields.ipv4_mode ? (
|
|
||||||
<EmptyCard
|
|
||||||
IconElm={LuEthernetPort}
|
|
||||||
headline="Pending DHCP IPv4 mode change"
|
|
||||||
description="Save settings to enable DHCP mode and view lease information"
|
|
||||||
/>
|
|
||||||
) : ipv4mode === "dhcp" ? (
|
) : ipv4mode === "dhcp" ? (
|
||||||
<DhcpLeaseCard
|
<DhcpLeaseCard
|
||||||
networkState={networkState}
|
networkState={networkState}
|
||||||
|
|
@ -439,91 +393,12 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
{/* Critical change confirm */}
|
|
||||||
<ConfirmDialog
|
|
||||||
open={showCriticalSettingsConfirm}
|
|
||||||
title="Apply network settings"
|
|
||||||
variant="warning"
|
|
||||||
confirmText="Apply changes"
|
|
||||||
onConfirm={() => {
|
|
||||||
setShowCriticalSettingsConfirm(false);
|
|
||||||
if (stagedSettings) onSubmit(stagedSettings);
|
|
||||||
|
|
||||||
// Wait for the close animation to finish before resetting the staged settings
|
|
||||||
setTimeout(() => {
|
|
||||||
setStagedSettings(null);
|
|
||||||
setCriticalChanges([]);
|
|
||||||
}, 500);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
// close();
|
|
||||||
setShowCriticalSettingsConfirm(false);
|
|
||||||
}}
|
|
||||||
isConfirming={formState.isSubmitting}
|
|
||||||
description={
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p>
|
|
||||||
This will update the device's network configuration and may briefly
|
|
||||||
disconnect your session.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 dark:border-slate-700 dark:bg-slate-900/40">
|
|
||||||
<div className="mb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400">
|
|
||||||
Pending changes
|
|
||||||
</div>
|
|
||||||
<dl className="grid grid-cols-1 gap-y-2">
|
|
||||||
{criticalChanges.map((c, idx) => (
|
|
||||||
<div key={idx} className="w-full not-last:pb-2">
|
|
||||||
<div className="flex items-center gap-2 gap-x-8">
|
|
||||||
<dt className="text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
{c.label}
|
|
||||||
</dt>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="rounded-sm bg-slate-200 px-1.5 py-0.5 text-sm font-medium text-slate-900 dark:bg-slate-700 dark:text-slate-100">
|
|
||||||
{c.from || "—"}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="rounded-sm bg-slate-200 px-1.5 py-0.5 text-sm font-medium text-slate-900 dark:bg-slate-700 dark:text-slate-100">
|
|
||||||
{c.to}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm">
|
|
||||||
If the network settings are invalid,{" "}
|
|
||||||
<strong>the device may become unreachable</strong> and require a factory
|
|
||||||
reset to restore connectivity.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showRenewLeaseConfirm}
|
open={showRenewLeaseConfirm}
|
||||||
title="Renew DHCP Lease"
|
title="Renew DHCP Lease"
|
||||||
variant="warning"
|
description="Are you sure you want to renew the DHCP lease? This may temporarily disconnect the device."
|
||||||
confirmText="Renew Lease"
|
|
||||||
description={
|
|
||||||
<p>
|
|
||||||
This will request a new IP address from your router. The device may briefly
|
|
||||||
disconnect during the renewal process.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
If you receive a new IP address,{" "}
|
|
||||||
<strong>you may need to reconnect using the new address</strong>.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
setShowRenewLeaseConfirm(false);
|
setShowRenewLeaseConfirm(false);
|
||||||
onDhcpLeaseRenew();
|
|
||||||
}}
|
}}
|
||||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue