mirror of https://github.com/jetkvm/kvm.git
feat(ui): enhance network settings form with react-hook-form integration
This commit is contained in:
parent
839201cc7a
commit
fe7450d6ec
|
@ -9,12 +9,12 @@ export default function DhcpLeaseCard({
|
||||||
networkState,
|
networkState,
|
||||||
setShowRenewLeaseConfirm,
|
setShowRenewLeaseConfirm,
|
||||||
}: {
|
}: {
|
||||||
networkState: NetworkState;
|
networkState: NetworkState | null;
|
||||||
setShowRenewLeaseConfirm: (show: boolean) => void;
|
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black 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">
|
||||||
<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
|
||||||
|
@ -44,24 +44,15 @@ export default function DhcpLeaseCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.dns && (
|
{networkState?.dhcp_lease?.dns_servers && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
DNS Servers
|
DNS Servers
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right text-sm font-medium">
|
<span className="text-right text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
{networkState?.dhcp_lease?.dns_servers.map(dns => (
|
||||||
</span>
|
<div key={dns}>{dns}</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.broadcast && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Broadcast
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.broadcast}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -142,6 +133,17 @@ export default function DhcpLeaseCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.broadcast && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.broadcast}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.mtu && (
|
{networkState?.dhcp_lease?.mtu && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { GridCard } from "./Card";
|
||||||
export default function Ipv6NetworkCard({
|
export default function Ipv6NetworkCard({
|
||||||
networkState,
|
networkState,
|
||||||
}: {
|
}: {
|
||||||
networkState: NetworkState;
|
networkState: NetworkState | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
|
|
|
@ -3,14 +3,19 @@ import { ReactNode } from "react";
|
||||||
export function SettingsPageHeader({
|
export function SettingsPageHeader({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
action,
|
||||||
}: {
|
}: {
|
||||||
title: string | ReactNode;
|
title: string | ReactNode;
|
||||||
description: string | ReactNode;
|
description: string | ReactNode;
|
||||||
|
action?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="select-none">
|
<div className="flex items-center justify-between gap-x-2 select-none">
|
||||||
<h2 className=" text-xl font-extrabold text-black dark:text-white">{title}</h2>
|
<div className="flex flex-col gap-y-1">
|
||||||
<div className="text-sm text-black dark:text-slate-300">{description}</div>
|
<h2 className="text-xl font-extrabold text-black dark:text-white">{title}</h2>
|
||||||
|
<div className="text-sm text-black dark:text-slate-300">{description}</div>
|
||||||
|
</div>
|
||||||
|
{action && <div className="">{action}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,111 +1,16 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { LuPlus, LuX } from "react-icons/lu";
|
import { LuPlus, LuX } from "react-icons/lu";
|
||||||
import isIP from "validator/es/lib/isIP";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
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 { IPv4StaticConfig, NetworkSettings, NetworkState } from "@/hooks/stores";
|
|
||||||
|
|
||||||
interface StaticIpv4CardProps {
|
export default function StaticIpv4Card() {
|
||||||
networkSettings: NetworkSettings;
|
const formMethods = useFormContext();
|
||||||
onUpdate: (settings: NetworkSettings) => void;
|
const { register, formState } = formMethods;
|
||||||
networkState?: NetworkState;
|
|
||||||
onApply: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function StaticIpv4Card({
|
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" });
|
||||||
networkSettings,
|
|
||||||
onUpdate,
|
|
||||||
networkState,
|
|
||||||
onApply,
|
|
||||||
}: StaticIpv4CardProps) {
|
|
||||||
const [staticConfig, setStaticConfig] = useState<IPv4StaticConfig>({
|
|
||||||
address: "",
|
|
||||||
netmask: "",
|
|
||||||
gateway: "",
|
|
||||||
dns: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validation errors
|
|
||||||
const addressError =
|
|
||||||
staticConfig.address && !isIP(staticConfig.address, "4") ? "Invalid IP address" : "";
|
|
||||||
const netmaskError =
|
|
||||||
staticConfig.netmask && !isIP(staticConfig.netmask, "4") ? "Invalid subnet mask" : "";
|
|
||||||
const gatewayError =
|
|
||||||
staticConfig.gateway && !isIP(staticConfig.gateway, "4")
|
|
||||||
? "Invalid gateway address"
|
|
||||||
: "";
|
|
||||||
const dnsErrors = staticConfig.dns.map(dns =>
|
|
||||||
dns && !isIP(dns, "4") ? "Invalid DNS server" : "",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if any field has an error or if required fields are empty
|
|
||||||
const hasValidationErrors = !!(
|
|
||||||
addressError ||
|
|
||||||
netmaskError ||
|
|
||||||
gatewayError ||
|
|
||||||
dnsErrors.some(error => error)
|
|
||||||
);
|
|
||||||
const hasEmptyRequiredFields =
|
|
||||||
!staticConfig.address ||
|
|
||||||
!staticConfig.netmask ||
|
|
||||||
!staticConfig.gateway ||
|
|
||||||
staticConfig.dns.length === 0;
|
|
||||||
const isFormValid = !hasValidationErrors && !hasEmptyRequiredFields;
|
|
||||||
|
|
||||||
// Initialize from existing settings or use current network state as defaults
|
|
||||||
useEffect(() => {
|
|
||||||
if (networkSettings.ipv4_static) {
|
|
||||||
setStaticConfig(networkSettings.ipv4_static);
|
|
||||||
} else if (networkState?.dhcp_lease) {
|
|
||||||
// Use current DHCP values as defaults
|
|
||||||
const defaults: IPv4StaticConfig = {
|
|
||||||
address: networkState.dhcp_lease.ip || "",
|
|
||||||
netmask: networkState.dhcp_lease.netmask || "",
|
|
||||||
gateway: networkState.dhcp_lease.routers?.[0] || "",
|
|
||||||
dns: networkState.dhcp_lease.dns_servers || [],
|
|
||||||
};
|
|
||||||
setStaticConfig(defaults);
|
|
||||||
// Update the parent with these default values
|
|
||||||
onUpdate({ ...networkSettings, ipv4_static: defaults });
|
|
||||||
}
|
|
||||||
}, [networkSettings.ipv4_static, networkState?.dhcp_lease, networkSettings, onUpdate]);
|
|
||||||
|
|
||||||
const handleConfigChange = (
|
|
||||||
field: keyof Omit<IPv4StaticConfig, "dns">,
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
const updatedConfig = { ...staticConfig, [field]: value };
|
|
||||||
setStaticConfig(updatedConfig);
|
|
||||||
onUpdate({ ...networkSettings, ipv4_static: updatedConfig });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDnsChange = (index: number, value: string) => {
|
|
||||||
const updatedDns = [...staticConfig.dns];
|
|
||||||
updatedDns[index] = value;
|
|
||||||
const updatedConfig = { ...staticConfig, dns: updatedDns };
|
|
||||||
setStaticConfig(updatedConfig);
|
|
||||||
onUpdate({ ...networkSettings, ipv4_static: updatedConfig });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddDnsField = () => {
|
|
||||||
const updatedConfig = {
|
|
||||||
...staticConfig,
|
|
||||||
dns: [...staticConfig.dns, ""],
|
|
||||||
};
|
|
||||||
setStaticConfig(updatedConfig);
|
|
||||||
onUpdate({ ...networkSettings, ipv4_static: updatedConfig });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveDnsServer = (index: number) => {
|
|
||||||
const updatedConfig = {
|
|
||||||
...staticConfig,
|
|
||||||
dns: staticConfig.dns.filter((_, i) => i !== index),
|
|
||||||
};
|
|
||||||
setStaticConfig(updatedConfig);
|
|
||||||
onUpdate({ ...networkSettings, ipv4_static: updatedConfig });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
|
@ -121,9 +26,7 @@ export default function StaticIpv4Card({
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="192.168.1.100"
|
placeholder="192.168.1.100"
|
||||||
value={staticConfig.address}
|
{...register("ipv4_static.address")}
|
||||||
onChange={e => handleConfigChange("address", e.target.value)}
|
|
||||||
error={addressError}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
|
@ -131,9 +34,7 @@ export default function StaticIpv4Card({
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="255.255.255.0"
|
placeholder="255.255.255.0"
|
||||||
value={staticConfig.netmask}
|
{...register("ipv4_static.netmask")}
|
||||||
onChange={e => handleConfigChange("netmask", e.target.value)}
|
|
||||||
error={netmaskError}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -142,96 +43,58 @@ export default function StaticIpv4Card({
|
||||||
type="text"
|
type="text"
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="192.168.1.1"
|
placeholder="192.168.1.1"
|
||||||
value={staticConfig.gateway}
|
{...register("ipv4_static.gateway")}
|
||||||
onChange={e => handleConfigChange("gateway", e.target.value)}
|
|
||||||
error={gatewayError}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* DNS server fields */}
|
{/* DNS server fields */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
{staticConfig.dns.length === 0 && (
|
{fields.map((dns, index) => {
|
||||||
<div className="flex items-center gap-2">
|
return (
|
||||||
<div className="flex-1">
|
<div key={dns.id}>
|
||||||
<InputFieldWithLabel
|
<div className="flex items-start gap-x-2">
|
||||||
label="Primary DNS Server"
|
<div className="flex-1">
|
||||||
type="text"
|
<InputFieldWithLabel
|
||||||
size="SM"
|
label={index === 0 ? "DNS Server" : null}
|
||||||
placeholder="8.8.8.8"
|
type="text"
|
||||||
value=""
|
size="SM"
|
||||||
onChange={e => {
|
placeholder="1.1.1.1"
|
||||||
const updatedConfig = { ...staticConfig, dns: [e.target.value] };
|
{...register(`ipv4_static.dns.${index}`, {
|
||||||
setStaticConfig(updatedConfig);
|
validate: (value: string) => {
|
||||||
onUpdate({ ...networkSettings, ipv4_static: updatedConfig });
|
if (value === "") return true;
|
||||||
}}
|
if (!validator.isIP(value)) return "Invalid IP address";
|
||||||
/>
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
error={formState.errors.ipv4_static?.dns?.[index]?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{index > 0 && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
LeadingIcon={LuX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
|
|
||||||
{staticConfig.dns.map((dns, index) => (
|
|
||||||
<StaticIpv4DnsField
|
|
||||||
key={index}
|
|
||||||
value={dns}
|
|
||||||
index={index}
|
|
||||||
isLast={index === staticConfig.dns.length - 1}
|
|
||||||
onChange={e => handleDnsChange(index, e)}
|
|
||||||
onAdd={handleAddDnsField}
|
|
||||||
onRemove={handleRemoveDnsServer}
|
|
||||||
error={dnsErrors[index]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
onClick={() => append("", { shouldFocus: true })}
|
||||||
|
LeadingIcon={LuPlus}
|
||||||
|
type="button"
|
||||||
|
text="Add DNS Server"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StaticIpv4DnsField({
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
isLast,
|
|
||||||
onChange,
|
|
||||||
onAdd,
|
|
||||||
onRemove,
|
|
||||||
error,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
index: number;
|
|
||||||
isLast: boolean;
|
|
||||||
onChange: (dns: string) => void;
|
|
||||||
onAdd: () => void;
|
|
||||||
onRemove: (index: number) => void;
|
|
||||||
error?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex items-center gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<InputFieldWithLabel
|
|
||||||
label={index === 0 ? "Primary DNS Server" : `DNS Server ${index + 1}`}
|
|
||||||
type="text"
|
|
||||||
size="SM"
|
|
||||||
placeholder="8.8.8.8"
|
|
||||||
value={value}
|
|
||||||
onChange={e => onChange(e.target.value)}
|
|
||||||
error={error || ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-[21.875px] flex-shrink-0">
|
|
||||||
{
|
|
||||||
// if last item, show add button
|
|
||||||
isLast ? (
|
|
||||||
<Button size="SM" theme="light" onClick={onAdd} LeadingIcon={LuPlus} />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="danger"
|
|
||||||
onClick={() => onRemove(index)}
|
|
||||||
LeadingIcon={LuX}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -436,7 +436,7 @@ export interface KeyboardLedState {
|
||||||
scroll_lock: boolean;
|
scroll_lock: boolean;
|
||||||
compose: boolean;
|
compose: boolean;
|
||||||
kana: boolean;
|
kana: boolean;
|
||||||
};
|
}
|
||||||
const defaultKeyboardLedState: KeyboardLedState = {
|
const defaultKeyboardLedState: KeyboardLedState = {
|
||||||
num_lock: false,
|
num_lock: false,
|
||||||
caps_lock: false,
|
caps_lock: false,
|
||||||
|
@ -516,7 +516,8 @@ export const useHidStore = create<HidState>((set, get) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
keyboardLedStateSyncAvailable: false,
|
keyboardLedStateSyncAvailable: false,
|
||||||
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
setKeyboardLedStateSyncAvailable: available =>
|
||||||
|
set({ keyboardLedStateSyncAvailable: available }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
@ -752,9 +753,9 @@ export interface IPv4StaticConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
hostname: string;
|
hostname: string | null;
|
||||||
domain: string;
|
domain: string | null;
|
||||||
http_proxy: string;
|
http_proxy: string | null;
|
||||||
ipv4_mode: IPv4Mode;
|
ipv4_mode: IPv4Mode;
|
||||||
ipv4_static?: IPv4StaticConfig;
|
ipv4_static?: IPv4StaticConfig;
|
||||||
ipv6_mode: IPv6Mode;
|
ipv6_mode: IPv6Mode;
|
||||||
|
@ -944,5 +945,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
--animate-fadeInScaleFloat:
|
--animate-fadeInScaleFloat:
|
||||||
fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite;
|
fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite;
|
||||||
--animate-fadeIn: fadeIn 1s ease-out forwards;
|
--animate-fadeIn: fadeIn 1s ease-out forwards;
|
||||||
|
--animate-fadeInStill: fadeInStill 1s ease-out forwards;
|
||||||
--animate-slideUpFade: slideUpFade 1s ease-out forwards;
|
--animate-slideUpFade: slideUpFade 1s ease-out forwards;
|
||||||
|
|
||||||
--container-8xl: 88rem;
|
--container-8xl: 88rem;
|
||||||
|
@ -110,6 +111,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInStill {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideUpFade {
|
@keyframes slideUpFade {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -1,49 +1,48 @@
|
||||||
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";
|
||||||
|
import { useForm, FormProvider, FieldValues } from "react-hook-form";
|
||||||
|
import validator from "validator";
|
||||||
|
|
||||||
import {
|
import { NetworkSettings, NetworkState, useRTCStore } from "@/hooks/stores";
|
||||||
IPv4Mode,
|
|
||||||
IPv6Mode,
|
|
||||||
LLDPMode,
|
|
||||||
mDNSMode,
|
|
||||||
NetworkSettings,
|
|
||||||
NetworkState,
|
|
||||||
TimeSyncMode,
|
|
||||||
useNetworkStateStore,
|
|
||||||
} from "@/hooks/stores";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
import Fieldset from "@/components/Fieldset";
|
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||||
|
|
||||||
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||||
import EmptyCard from "../components/EmptyCard";
|
import EmptyCard from "../components/EmptyCard";
|
||||||
import AutoHeight from "../components/AutoHeight";
|
import AutoHeight from "../components/AutoHeight";
|
||||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||||
import StaticIpv4Card from "../components/StaticIpv4Card";
|
import StaticIpv4Card from "../components/StaticIpv4Card";
|
||||||
|
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const defaultNetworkSettings: NetworkSettings = {
|
const resolveOnRtcReady = () => {
|
||||||
hostname: "",
|
return new Promise(resolve => {
|
||||||
http_proxy: "",
|
// Check if RTC is already connected
|
||||||
domain: "",
|
const currentState = useRTCStore.getState();
|
||||||
ipv4_mode: "unknown",
|
if (currentState.rpcDataChannel?.readyState === "open") {
|
||||||
ipv4_static: undefined,
|
// Already connected, fetch data immediately
|
||||||
ipv6_mode: "unknown",
|
return resolve(void 0);
|
||||||
lldp_mode: "unknown",
|
}
|
||||||
lldp_tx_tlvs: [],
|
|
||||||
mdns_mode: "unknown",
|
// Not connected yet, subscribe to state changes
|
||||||
time_sync_mode: "unknown",
|
const unsubscribe = useRTCStore.subscribe(state => {
|
||||||
|
if (state.rpcDataChannel?.readyState === "open") {
|
||||||
|
unsubscribe(); // Clean up subscription
|
||||||
|
return resolve(void 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
@ -75,434 +74,311 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
|
||||||
export default function SettingsNetworkRoute() {
|
export default function SettingsNetworkRoute() {
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
|
||||||
state,
|
|
||||||
state.setNetworkState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [networkSettings, setNetworkSettings] =
|
const [networkState, setNetworkState] = useState<NetworkState | null>(null);
|
||||||
useState<NetworkSettings>(defaultNetworkSettings);
|
|
||||||
|
|
||||||
// We use this to determine whether the settings have changed
|
|
||||||
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
|
|
||||||
|
|
||||||
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
|
||||||
|
|
||||||
|
// Some input needs direct state management. Mostly options that open more details
|
||||||
const [customDomain, setCustomDomain] = useState<string>("");
|
const [customDomain, setCustomDomain] = useState<string>("");
|
||||||
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (networkSettings.domain && networkSettingsLoaded) {
|
|
||||||
// Check if the domain is one of the predefined options
|
|
||||||
const predefinedOptions = ["dhcp", "local"];
|
|
||||||
if (predefinedOptions.includes(networkSettings.domain)) {
|
|
||||||
setSelectedDomainOption(networkSettings.domain);
|
|
||||||
} else {
|
|
||||||
setSelectedDomainOption("custom");
|
|
||||||
setCustomDomain(networkSettings.domain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
|
||||||
|
|
||||||
const getNetworkSettings = useCallback(() => {
|
|
||||||
setNetworkSettingsLoaded(false);
|
|
||||||
send("getNetworkSettings", {}, resp => {
|
|
||||||
if ("error" in resp) return;
|
|
||||||
console.log(resp.result);
|
|
||||||
setNetworkSettings(resp.result as NetworkSettings);
|
|
||||||
|
|
||||||
if (!firstNetworkSettings.current) {
|
|
||||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
|
||||||
}
|
|
||||||
setNetworkSettingsLoaded(true);
|
|
||||||
});
|
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
const getNetworkState = useCallback(() => {
|
|
||||||
send("getNetworkState", {}, resp => {
|
|
||||||
if ("error" in resp) return;
|
|
||||||
console.log(resp.result);
|
|
||||||
setNetworkState(resp.result as NetworkState);
|
|
||||||
});
|
|
||||||
}, [send, setNetworkState]);
|
|
||||||
|
|
||||||
const setNetworkSettingsRemote = useCallback(
|
|
||||||
(settings: NetworkSettings) => {
|
|
||||||
setNetworkSettingsLoaded(false);
|
|
||||||
|
|
||||||
// Filter out empty DNS strings from static IP config before sending
|
|
||||||
const filteredSettings = { ...settings };
|
|
||||||
if (filteredSettings.ipv4_static?.dns) {
|
|
||||||
filteredSettings.ipv4_static = {
|
|
||||||
...filteredSettings.ipv4_static,
|
|
||||||
dns: filteredSettings.ipv4_static.dns.filter(dns => dns.trim() !== "")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
send("setNetworkSettings", { settings: filteredSettings }, resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error(
|
|
||||||
"Failed to save network settings: " +
|
|
||||||
(resp.error.data ? resp.error.data : resp.error.message),
|
|
||||||
);
|
|
||||||
setNetworkSettingsLoaded(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
|
||||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
|
||||||
setNetworkSettings(resp.result as NetworkSettings);
|
|
||||||
getNetworkState();
|
|
||||||
setNetworkSettingsLoaded(true);
|
|
||||||
notifications.success("Network settings saved");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getNetworkState, send],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRenewLease = useCallback(() => {
|
|
||||||
send("renewDHCPLease", {}, resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error("Failed to renew lease: " + resp.error.message);
|
|
||||||
} else {
|
|
||||||
notifications.success("DHCP lease renewed");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getNetworkState();
|
|
||||||
getNetworkSettings();
|
|
||||||
}, [getNetworkState, getNetworkSettings]);
|
|
||||||
|
|
||||||
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIpv6ModeChange = (value: IPv6Mode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLldpModeChange = (value: LLDPMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeSyncModeChange = (value: TimeSyncMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHostnameChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, hostname: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProxyChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, http_proxy: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDomainChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, domain: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDomainOptionChange = (value: string) => {
|
|
||||||
setSelectedDomainOption(value);
|
|
||||||
if (value !== "custom") {
|
|
||||||
handleDomainChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCustomDomainChange = (value: string) => {
|
|
||||||
setCustomDomain(value);
|
|
||||||
handleDomainChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterUnknown = useCallback(
|
|
||||||
(options: { value: string; label: string }[]) => {
|
|
||||||
if (!networkSettingsLoaded) return options;
|
|
||||||
return options.filter(option => option.value !== "unknown");
|
|
||||||
},
|
|
||||||
[networkSettingsLoaded],
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Confirm dialog
|
||||||
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
||||||
|
|
||||||
|
const fetchNetworkData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log("Fetching network data...");
|
||||||
|
|
||||||
|
const [settings, state] = (await Promise.all([
|
||||||
|
getNetworkSettings(),
|
||||||
|
getNetworkState(),
|
||||||
|
])) as [NetworkSettings, NetworkState];
|
||||||
|
|
||||||
|
setNetworkState(state as NetworkState);
|
||||||
|
|
||||||
|
const settingsWithDefaults = {
|
||||||
|
...settings,
|
||||||
|
|
||||||
|
domain: settings.domain || "local", // TODO: null means local domain TRUE?????
|
||||||
|
mdns_mode: settings.mdns_mode || "disabled",
|
||||||
|
time_sync_mode: settings.time_sync_mode || "ntp_only",
|
||||||
|
ipv4_static: {
|
||||||
|
address: settings.ipv4_static?.address || state.dhcp_lease?.ip || "",
|
||||||
|
netmask: settings.ipv4_static?.netmask || state.dhcp_lease?.netmask || "",
|
||||||
|
gateway: settings.ipv4_static?.gateway || state.dhcp_lease?.routers?.[0] || "",
|
||||||
|
dns: settings.ipv4_static?.dns || state.dhcp_lease?.dns_servers || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { settings: settingsWithDefaults, state };
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formMethods = useForm<NetworkSettings>({
|
||||||
|
mode: "onBlur",
|
||||||
|
|
||||||
|
defaultValues: async () => {
|
||||||
|
console.log("Preparing form default values...");
|
||||||
|
|
||||||
|
// Ensure data channel is ready, before fetching network data from the device
|
||||||
|
await resolveOnRtcReady();
|
||||||
|
|
||||||
|
const { settings } = await fetchNetworkData();
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
||||||
|
|
||||||
|
const onSubmit = async (data: FieldValues) => {
|
||||||
|
const settings = {
|
||||||
|
...data,
|
||||||
|
|
||||||
|
// If custom domain option is selected, use the custom domain as value
|
||||||
|
domain: data.domain === "custom" ? customDomain : data.domain,
|
||||||
|
ipv4_static: {
|
||||||
|
...data.ipv4_static,
|
||||||
|
|
||||||
|
// Remove empty DNS entries
|
||||||
|
dns: data.ipv4_static?.dns.filter((dns: string) => dns.trim() !== ""),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
send("setNetworkSettings", { settings }, async resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
return notifications.error(
|
||||||
|
resp.error.data ? resp.error.data : resp.error.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If the settings are saved successfully, fetch the latest network data and reset the form
|
||||||
|
// We do this so we get all the form state values, for stuff like is the form dirty, etc...
|
||||||
|
const networkData = await fetchNetworkData();
|
||||||
|
reset(networkData.settings);
|
||||||
|
notifications.success("Network settings saved");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIPv4Mode = watch("ipv4_mode");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Fieldset disabled={!networkSettingsLoaded} className="space-y-4">
|
<FormProvider {...formMethods}>
|
||||||
<SettingsPageHeader
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
title="Network"
|
<SettingsPageHeader
|
||||||
description="Configure your network settings"
|
title="Network"
|
||||||
/>
|
description="Configure the network settings for the device"
|
||||||
<div className="space-y-4">
|
action={
|
||||||
<SettingsItem
|
<>
|
||||||
title="MAC Address"
|
{(formState.isDirty || formState.isSubmitting) && (
|
||||||
description="Hardware identifier for the network interface"
|
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||||
>
|
<Button
|
||||||
<InputField
|
size="SM"
|
||||||
type="text"
|
theme="primary"
|
||||||
size="SM"
|
disabled={formState.isSubmitting}
|
||||||
value={networkState?.mac_address}
|
loading={formState.isSubmitting}
|
||||||
error={""}
|
type="submit"
|
||||||
readOnly={true}
|
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||||
className="dark:!text-opacity-60"
|
/>
|
||||||
/>
|
</div>
|
||||||
</SettingsItem>
|
)}
|
||||||
</div>
|
</>
|
||||||
<div className="space-y-4">
|
}
|
||||||
<SettingsItem
|
/>
|
||||||
title="Hostname"
|
|
||||||
description="Device identifier on the network. Blank for system default"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div>
|
|
||||||
<InputField
|
|
||||||
size="SM"
|
|
||||||
type="text"
|
|
||||||
placeholder="jetkvm"
|
|
||||||
defaultValue={networkSettings.hostname}
|
|
||||||
onChange={e => {
|
|
||||||
handleHostnameChange(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
|
||||||
title="HTTP Proxy"
|
|
||||||
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div>
|
|
||||||
<InputField
|
|
||||||
size="SM"
|
|
||||||
type="text"
|
|
||||||
placeholder="http://proxy.example.com:8080/"
|
|
||||||
defaultValue={networkSettings.http_proxy}
|
|
||||||
onChange={e => {
|
|
||||||
handleProxyChange(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<SettingsItem
|
|
||||||
title="Domain"
|
|
||||||
description="Network domain suffix for the device"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
value={selectedDomainOption}
|
|
||||||
onChange={e => handleDomainOptionChange(e.target.value)}
|
|
||||||
options={[
|
|
||||||
{ value: "dhcp", label: "DHCP provided" },
|
|
||||||
{ value: "local", label: ".local" },
|
|
||||||
{ value: "custom", label: "Custom" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
|
||||||
{selectedDomainOption === "custom" && (
|
|
||||||
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
|
||||||
<InputFieldWithLabel
|
|
||||||
size="SM"
|
|
||||||
type="text"
|
|
||||||
label="Custom Domain"
|
|
||||||
placeholder="home"
|
|
||||||
value={customDomain}
|
|
||||||
onChange={e => {
|
|
||||||
setCustomDomain(e.target.value);
|
|
||||||
handleCustomDomainChange(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="mDNS"
|
title="MAC Address"
|
||||||
description="Control mDNS (multicast DNS) operational mode"
|
description="Hardware identifier for the network interface"
|
||||||
>
|
>
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
value={networkState?.mac_address}
|
||||||
|
error={""}
|
||||||
|
readOnly={true}
|
||||||
|
className="dark:!text-opacity-60"
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<SettingsItem title="Hostname" description="Set the device hostname">
|
||||||
|
<InputField
|
||||||
|
size="SM"
|
||||||
|
{...register("hostname")}
|
||||||
|
error={formState.errors.hostname?.message}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<SettingsItem title="HTTP Proxy" description="Configure HTTP proxy settings">
|
||||||
|
<InputField
|
||||||
|
size="SM"
|
||||||
|
placeholder="http://proxy.example.com:8080"
|
||||||
|
{...register("http_proxy", {
|
||||||
|
validate: (value: string | null) => {
|
||||||
|
if (value === "") return true;
|
||||||
|
if (!validator.isURL(value || "", { protocols: ["http", "https"] })) {
|
||||||
|
return "Invalid HTTP proxy URL";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
error={formState.errors.http_proxy?.message}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<SettingsItem
|
||||||
|
title="Domain"
|
||||||
|
description="Network domain suffix for the device"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
options={[
|
||||||
|
{ value: "dhcp", label: "DHCP provided" },
|
||||||
|
{ value: "local", label: ".local" },
|
||||||
|
{ value: "custom", label: "Custom" },
|
||||||
|
]}
|
||||||
|
{...register("domain")}
|
||||||
|
error={formState.errors.domain?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsItem>
|
||||||
|
{watch("domain") === "custom" && (
|
||||||
|
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
||||||
|
<InputFieldWithLabel
|
||||||
|
size="SM"
|
||||||
|
type="text"
|
||||||
|
label="Custom Domain"
|
||||||
|
placeholder="home"
|
||||||
|
onChange={e => {
|
||||||
|
setCustomDomain(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsItem title="mDNS Mode" description="Configure mDNS settings">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.mdns_mode}
|
options={[
|
||||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "disabled", label: "Disabled" },
|
{ value: "disabled", label: "Disabled" },
|
||||||
{ value: "auto", label: "Auto" },
|
{ value: "auto", label: "Auto" },
|
||||||
{ value: "ipv4_only", label: "IPv4 only" },
|
{ value: "ipv4_only", label: "IPv4 only" },
|
||||||
{ value: "ipv6_only", label: "IPv6 only" },
|
{ value: "ipv6_only", label: "IPv6 only" },
|
||||||
])}
|
]}
|
||||||
|
{...register("mdns_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Time synchronization"
|
title="Time synchronization"
|
||||||
description="Configure time synchronization settings"
|
description="Configure time synchronization settings"
|
||||||
>
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.time_sync_mode}
|
options={[
|
||||||
onChange={e => {
|
|
||||||
handleTimeSyncModeChange(e.target.value);
|
|
||||||
}}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "unknown", label: "..." },
|
|
||||||
// { value: "auto", label: "Auto" },
|
|
||||||
{ value: "ntp_only", label: "NTP only" },
|
{ value: "ntp_only", label: "NTP only" },
|
||||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||||
{ value: "http_only", label: "HTTP only" },
|
{ value: "http_only", label: "HTTP only" },
|
||||||
// { value: "custom", label: "Custom" },
|
]}
|
||||||
])}
|
{...register("time_sync_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||||
size="SM"
|
<SelectMenuBasic
|
||||||
theme="primary"
|
size="SM"
|
||||||
disabled={firstNetworkSettings.current === networkSettings}
|
options={[
|
||||||
text="Save Settings"
|
{ value: "dhcp", label: "DHCP" },
|
||||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
{ value: "static", label: "Static" },
|
||||||
/>
|
]}
|
||||||
</div>
|
{...register("ipv4_mode")}
|
||||||
|
|
||||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
value={networkSettings.ipv4_mode}
|
|
||||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "dhcp", label: "DHCP" },
|
|
||||||
{ value: "static", label: "Static" },
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
<AutoHeight>
|
|
||||||
{!networkSettingsLoaded && !networkState?.dhcp_lease ? (
|
|
||||||
<GridCard>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
|
||||||
DHCP Lease Information
|
|
||||||
</h3>
|
|
||||||
<div className="animate-pulse space-y-3">
|
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
<div classNa
|
|
||||||
onApply={() => setNetworkSettingsRemote(networkSettings)} </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GridCard>
|
|
||||||
) : networkSettings.ipv4_mode === "static" ? (
|
|
||||||
<StaticIpv4Card
|
|
||||||
networkSettings={networkSettings}
|
|
||||||
onUpdate={setNetworkSettings}
|
|
||||||
networkState={networkState}
|
|
||||||
onApply={() => setNetworkSettingsRemote(networkSettings)}
|
|
||||||
/>
|
/>
|
||||||
) : networkSettings.ipv4_mode === "dhcp" &&
|
</SettingsItem>
|
||||||
networkState?.dhcp_lease &&
|
<div>
|
||||||
networkState.dhcp_lease.ip ? (
|
<AutoHeight>
|
||||||
<DhcpLeaseCard
|
{formState.isLoading ? (
|
||||||
networkState={networkState}
|
<GridCard>
|
||||||
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
<div className="p-4">
|
||||||
/>
|
<div className="space-y-4">
|
||||||
) : (
|
<div className="h-6 w-1/3 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<EmptyCard
|
<div className="animate-pulse space-y-2">
|
||||||
IconElm={LuEthernetPort}
|
<div className="h-4 w-1/4 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
headline="Network Information"
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
description="No network configuration available"
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
/>
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
)}
|
<div className="h-4 w-1/4 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
</AutoHeight>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
value={networkSettings.ipv6_mode}
|
|
||||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
// { value: "disabled", label: "Disabled" },
|
|
||||||
{ value: "slaac", label: "SLAAC" },
|
|
||||||
// { value: "dhcpv6", label: "DHCPv6" },
|
|
||||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
|
||||||
// { value: "static", label: "Static" },
|
|
||||||
// { value: "link_local", label: "Link-local only" },
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
<AutoHeight>
|
|
||||||
{!networkSettingsLoaded &&
|
|
||||||
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
|
|
||||||
<GridCard>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
|
||||||
IPv6 Information
|
|
||||||
</h3>
|
|
||||||
<div className="animate-pulse space-y-3">
|
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GridCard>
|
||||||
</div>
|
) : isIPv4Mode === "static" ? (
|
||||||
</GridCard>
|
<StaticIpv4Card />
|
||||||
) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? (
|
) : isIPv4Mode === "dhcp" ? (
|
||||||
<Ipv6NetworkCard networkState={networkState} />
|
<DhcpLeaseCard
|
||||||
) : (
|
networkState={networkState}
|
||||||
<EmptyCard
|
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||||
IconElm={LuEthernetPort}
|
/>
|
||||||
headline="IPv6 Information"
|
) : (
|
||||||
description="No IPv6 addresses configured"
|
<EmptyCard
|
||||||
|
IconElm={LuEthernetPort}
|
||||||
|
headline="Network Information"
|
||||||
|
description="No network configuration available"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoHeight>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
options={[{ value: "slaac", label: "SLAAC" }]}
|
||||||
|
{...register("ipv6_mode")}
|
||||||
/>
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<AutoHeight>
|
||||||
|
{!networkState?.ipv6_addresses ? (
|
||||||
|
<GridCard>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Network Information
|
||||||
|
</h3>
|
||||||
|
<div className="animate-pulse space-y-3">
|
||||||
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
) : (
|
||||||
|
<Ipv6NetworkCard networkState={networkState || undefined} />
|
||||||
|
)}
|
||||||
|
</AutoHeight>
|
||||||
|
</div>
|
||||||
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
{(formState.isDirty || formState.isSubmitting) && (
|
||||||
|
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
disabled={formState.isSubmitting}
|
||||||
|
loading={formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</AutoHeight>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<div className="hidden space-y-4">
|
</FormProvider>
|
||||||
<SettingsItem
|
|
||||||
title="LLDP"
|
|
||||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
|
||||||
>
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
value={networkSettings.lldp_mode}
|
|
||||||
onChange={e => handleLldpModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "disabled", label: "Disabled" },
|
|
||||||
{ value: "basic", label: "Basic" },
|
|
||||||
{ value: "all", label: "All" },
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
|
||||||
</Fieldset>
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showRenewLeaseConfirm}
|
open={showRenewLeaseConfirm}
|
||||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
|
||||||
title="Renew DHCP Lease"
|
title="Renew DHCP Lease"
|
||||||
description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process."
|
description="Are you sure you want to renew the DHCP lease? This may temporarily disconnect the device."
|
||||||
variant="danger"
|
|
||||||
confirmText="Renew Lease"
|
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
handleRenewLease();
|
|
||||||
setShowRenewLeaseConfirm(false);
|
setShowRenewLeaseConfirm(false);
|
||||||
}}
|
}}
|
||||||
|
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
// JSON-RPC utility for use outside of React components
|
||||||
|
export interface JsonRpcCallOptions {
|
||||||
|
method: string;
|
||||||
|
params?: unknown;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonRpcCallResponse {
|
||||||
|
jsonrpc: string;
|
||||||
|
result?: unknown;
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
id: number | string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpcCallCounter = 0;
|
||||||
|
|
||||||
|
export function callJsonRpc(options: JsonRpcCallOptions): Promise<JsonRpcCallResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Access the RTC store directly outside of React context
|
||||||
|
const rpcDataChannel = useRTCStore.getState().rpcDataChannel;
|
||||||
|
|
||||||
|
if (!rpcDataChannel || rpcDataChannel.readyState !== "open") {
|
||||||
|
reject(new Error("RPC data channel not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcCallCounter++;
|
||||||
|
const requestId = `rpc_${Date.now()}_${rpcCallCounter}`;
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: options.method,
|
||||||
|
params: options.params || {},
|
||||||
|
id: requestId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = options.timeout || 5000;
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(event.data) as JsonRpcCallResponse;
|
||||||
|
if (response.id === requestId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore parse errors from other messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
|
reject(new Error(`JSON-RPC call timed out after ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
rpcDataChannel.addEventListener("message", messageHandler);
|
||||||
|
rpcDataChannel.send(JSON.stringify(request));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific network settings API calls
|
||||||
|
export async function getNetworkSettings() {
|
||||||
|
const response = await callJsonRpc({ method: "getNetworkSettings" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setNetworkSettings(settings: unknown) {
|
||||||
|
const response = await callJsonRpc({
|
||||||
|
method: "setNetworkSettings",
|
||||||
|
params: { settings },
|
||||||
|
});
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNetworkState() {
|
||||||
|
const response = await callJsonRpc({ method: "getNetworkState" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renewDHCPLease() {
|
||||||
|
const response = await callJsonRpc({ method: "renewDHCPLease" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
Loading…
Reference in New Issue