diff --git a/ui/src/components/DhcpLeaseCard.tsx b/ui/src/components/DhcpLeaseCard.tsx index 8a6e59c..921520f 100644 --- a/ui/src/components/DhcpLeaseCard.tsx +++ b/ui/src/components/DhcpLeaseCard.tsx @@ -9,12 +9,12 @@ export default function DhcpLeaseCard({ networkState, setShowRenewLeaseConfirm, }: { - networkState: NetworkState; + networkState: NetworkState | null; setShowRenewLeaseConfirm: (show: boolean) => void; }) { return ( -
+

DHCP Lease Information @@ -44,24 +44,15 @@ export default function DhcpLeaseCard({

)} - {networkState?.dhcp_lease?.dns && ( + {networkState?.dhcp_lease?.dns_servers && (
DNS Servers - {networkState?.dhcp_lease?.dns.map(dns =>
{dns}
)} -
-
- )} - - {networkState?.dhcp_lease?.broadcast && ( -
- - Broadcast - - - {networkState?.dhcp_lease?.broadcast} + {networkState?.dhcp_lease?.dns_servers.map(dns => ( +
{dns}
+ ))}
)} @@ -142,6 +133,17 @@ export default function DhcpLeaseCard({
)} + {networkState?.dhcp_lease?.broadcast && ( +
+ + Broadcast + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + {networkState?.dhcp_lease?.mtu && (
MTU diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx index a31b78e..fabb7b6 100644 --- a/ui/src/components/Ipv6NetworkCard.tsx +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -6,7 +6,7 @@ import { GridCard } from "./Card"; export default function Ipv6NetworkCard({ networkState, }: { - networkState: NetworkState; + networkState: NetworkState | undefined; }) { return ( diff --git a/ui/src/components/SettingsPageheader.tsx b/ui/src/components/SettingsPageheader.tsx index a7e2621..580c57d 100644 --- a/ui/src/components/SettingsPageheader.tsx +++ b/ui/src/components/SettingsPageheader.tsx @@ -3,14 +3,19 @@ import { ReactNode } from "react"; export function SettingsPageHeader({ title, description, + action, }: { title: string | ReactNode; description: string | ReactNode; + action?: ReactNode; }) { return ( -
-

{title}

-
{description}
+
+
+

{title}

+
{description}
+
+ {action &&
{action}
}
); } diff --git a/ui/src/components/StaticIpv4Card.tsx b/ui/src/components/StaticIpv4Card.tsx index b7809fe..f7913c9 100644 --- a/ui/src/components/StaticIpv4Card.tsx +++ b/ui/src/components/StaticIpv4Card.tsx @@ -1,111 +1,16 @@ -import { useState, useEffect } from "react"; 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 { Button } from "@/components/Button"; import { InputFieldWithLabel } from "@/components/InputField"; -import { IPv4StaticConfig, NetworkSettings, NetworkState } from "@/hooks/stores"; -interface StaticIpv4CardProps { - networkSettings: NetworkSettings; - onUpdate: (settings: NetworkSettings) => void; - networkState?: NetworkState; - onApply: () => void; -} +export default function StaticIpv4Card() { + const formMethods = useFormContext(); + const { register, formState } = formMethods; -export default function StaticIpv4Card({ - networkSettings, - onUpdate, - networkState, - onApply, -}: StaticIpv4CardProps) { - const [staticConfig, setStaticConfig] = useState({ - 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, - 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 }); - }; + const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" }); return ( @@ -121,9 +26,7 @@ export default function StaticIpv4Card({ type="text" size="SM" placeholder="192.168.1.100" - value={staticConfig.address} - onChange={e => handleConfigChange("address", e.target.value)} - error={addressError} + {...register("ipv4_static.address")} /> handleConfigChange("netmask", e.target.value)} - error={netmaskError} + {...register("ipv4_static.netmask")} />
@@ -142,96 +43,58 @@ export default function StaticIpv4Card({ type="text" size="SM" placeholder="192.168.1.1" - value={staticConfig.gateway} - onChange={e => handleConfigChange("gateway", e.target.value)} - error={gatewayError} + {...register("ipv4_static.gateway")} /> {/* DNS server fields */} -
- {staticConfig.dns.length === 0 && ( -
-
- { - const updatedConfig = { ...staticConfig, dns: [e.target.value] }; - setStaticConfig(updatedConfig); - onUpdate({ ...networkSettings, ipv4_static: updatedConfig }); - }} - /> +
+ {fields.map((dns, index) => { + return ( +
+
+
+ { + if (value === "") return true; + if (!validator.isIP(value)) return "Invalid IP address"; + return true; + }, + })} + error={formState.errors.ipv4_static?.dns?.[index]?.message} + /> +
+ {index > 0 && ( +
+
+ )} +
-
- )} - - {staticConfig.dns.map((dns, index) => ( - handleDnsChange(index, e)} - onAdd={handleAddDnsField} - onRemove={handleRemoveDnsServer} - error={dnsErrors[index]} - /> - ))} + ); + })}
+ +
); } - -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 ( -
-
- onChange(e.target.value)} - error={error || ""} - /> -
-
- { - // if last item, show add button - isLast ? ( -
-
- ); -} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 565244e..e75fab4 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -436,7 +436,7 @@ export interface KeyboardLedState { scroll_lock: boolean; compose: boolean; kana: boolean; -}; +} const defaultKeyboardLedState: KeyboardLedState = { num_lock: false, caps_lock: false, @@ -516,7 +516,8 @@ export const useHidStore = create((set, get) => ({ }, keyboardLedStateSyncAvailable: false, - setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }), + setKeyboardLedStateSyncAvailable: available => + set({ keyboardLedStateSyncAvailable: available }), isVirtualKeyboardEnabled: false, setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), @@ -752,9 +753,9 @@ export interface IPv4StaticConfig { } export interface NetworkSettings { - hostname: string; - domain: string; - http_proxy: string; + hostname: string | null; + domain: string | null; + http_proxy: string | null; ipv4_mode: IPv4Mode; ipv4_static?: IPv4StaticConfig; ipv6_mode: IPv6Mode; @@ -944,5 +945,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - } + }, })); diff --git a/ui/src/index.css b/ui/src/index.css index 44acd2a..9021575 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -32,6 +32,7 @@ --animate-fadeInScaleFloat: fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite; --animate-fadeIn: fadeIn 1s ease-out forwards; + --animate-fadeInStill: fadeInStill 1s ease-out forwards; --animate-slideUpFade: slideUpFade 1s ease-out forwards; --container-8xl: 88rem; @@ -110,6 +111,15 @@ } } + @keyframes fadeInStill { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes slideUpFade { 0% { opacity: 0; diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 4139a0d..a80d4d6 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -1,49 +1,48 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { LuEthernetPort } from "react-icons/lu"; +import { useForm, FormProvider, FieldValues } from "react-hook-form"; +import validator from "validator"; -import { - IPv4Mode, - IPv6Mode, - LLDPMode, - mDNSMode, - NetworkSettings, - NetworkState, - TimeSyncMode, - useNetworkStateStore, -} from "@/hooks/stores"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { NetworkSettings, NetworkState, useRTCStore } from "@/hooks/stores"; import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; import InputField, { InputFieldWithLabel } from "@components/InputField"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; -import Fieldset from "@/components/Fieldset"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import notifications from "@/notifications"; +import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc"; import Ipv6NetworkCard from "../components/Ipv6NetworkCard"; import EmptyCard from "../components/EmptyCard"; import AutoHeight from "../components/AutoHeight"; import DhcpLeaseCard from "../components/DhcpLeaseCard"; import StaticIpv4Card from "../components/StaticIpv4Card"; +import { useJsonRpc } from "../hooks/useJsonRpc"; import { SettingsItem } from "./devices.$id.settings"; dayjs.extend(relativeTime); -const defaultNetworkSettings: NetworkSettings = { - hostname: "", - http_proxy: "", - domain: "", - ipv4_mode: "unknown", - ipv4_static: undefined, - ipv6_mode: "unknown", - lldp_mode: "unknown", - lldp_tx_tlvs: [], - mdns_mode: "unknown", - time_sync_mode: "unknown", +const resolveOnRtcReady = () => { + return new Promise(resolve => { + // Check if RTC is already connected + const currentState = useRTCStore.getState(); + if (currentState.rpcDataChannel?.readyState === "open") { + // Already connected, fetch data immediately + return resolve(void 0); + } + + // Not connected yet, subscribe to state changes + const unsubscribe = useRTCStore.subscribe(state => { + if (state.rpcDataChannel?.readyState === "open") { + unsubscribe(); // Clean up subscription + return resolve(void 0); + } + }); + }); }; export function LifeTimeLabel({ lifetime }: { lifetime: string }) { @@ -75,434 +74,311 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { export default function SettingsNetworkRoute() { const [send] = useJsonRpc(); - const [networkState, setNetworkState] = useNetworkStateStore(state => [ - state, - state.setNetworkState, - ]); - const [networkSettings, setNetworkSettings] = - useState(defaultNetworkSettings); - - // We use this to determine whether the settings have changed - const firstNetworkSettings = useRef(undefined); - - const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); + const [networkState, setNetworkState] = useState(null); + // Some input needs direct state management. Mostly options that open more details const [customDomain, setCustomDomain] = useState(""); - const [selectedDomainOption, setSelectedDomainOption] = useState("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 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({ + 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 ( <> -
- -
- - - -
-
- -
-
- { - handleHostnameChange(e.target.value); - }} - /> -
-
-
-
-
- -
-
- { - handleProxyChange(e.target.value); - }} - /> -
-
-
-
- -
-
- -
- handleDomainOptionChange(e.target.value)} - options={[ - { value: "dhcp", label: "DHCP provided" }, - { value: "local", label: ".local" }, - { value: "custom", label: "Custom" }, - ]} - /> -
-
- {selectedDomainOption === "custom" && ( -
- { - setCustomDomain(e.target.value); - handleCustomDomainChange(e.target.value); - }} - /> -
- )} -
+ +
+ + {(formState.isDirty || formState.isSubmitting) && ( +
+
+ )} + + } + />
+ + + + + + + { + if (value === "") return true; + if (!validator.isURL(value || "", { protocols: ["http", "https"] })) { + return "Invalid HTTP proxy URL"; + } + return true; + }, + })} + error={formState.errors.http_proxy?.message} + /> + +
+ +
+ +
+
+ {watch("domain") === "custom" && ( +
+ { + setCustomDomain(e.target.value); + }} + /> +
+ )} +
+ + handleMdnsModeChange(e.target.value)} - options={filterUnknown([ + options={[ { value: "disabled", label: "Disabled" }, { value: "auto", label: "Auto" }, { value: "ipv4_only", label: "IPv4 only" }, { value: "ipv6_only", label: "IPv6 only" }, - ])} + ]} + {...register("mdns_mode")} /> -
- -
{ - handleTimeSyncModeChange(e.target.value); - }} - options={filterUnknown([ - { value: "unknown", label: "..." }, - // { value: "auto", label: "Auto" }, + options={[ { value: "ntp_only", label: "NTP only" }, { value: "ntp_and_http", label: "NTP and HTTP" }, { value: "http_only", label: "HTTP only" }, - // { value: "custom", label: "Custom" }, - ])} + ]} + {...register("time_sync_mode")} /> -
-
- -
- -
- - handleIpv4ModeChange(e.target.value)} - options={filterUnknown([ - { value: "dhcp", label: "DHCP" }, - { value: "static", label: "Static" }, - ])} - /> - - - {!networkSettingsLoaded && !networkState?.dhcp_lease ? ( - -
-
-

- DHCP Lease Information -

-
-
-
-
setNetworkSettingsRemote(networkSettings)}
-
-
- - ) : networkSettings.ipv4_mode === "static" ? ( - setNetworkSettingsRemote(networkSettings)} + + - ) : networkSettings.ipv4_mode === "dhcp" && - networkState?.dhcp_lease && - networkState.dhcp_lease.ip ? ( - - ) : ( - - )} - -
-
- - 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" }, - ])} - /> - - - {!networkSettingsLoaded && - !(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? ( - -
-
-

- IPv6 Information -

-
-
-
-
+ +
+ + {formState.isLoading ? ( + +
+
+
+
+
+
+
+
+
+
+
-
-
- - ) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? ( - - ) : ( - + ) : isIPv4Mode === "static" ? ( + + ) : isIPv4Mode === "dhcp" ? ( + + ) : ( + + )} + +
+ + + + +
+ + {!networkState?.ipv6_addresses ? ( + +
+
+

+ IPv6 Network Information +

+
+
+
+
+
+
+
+ + ) : ( + + )} + +
+
+ {(formState.isDirty || formState.isSubmitting) && ( +
+
)} - -
-
- - handleLldpModeChange(e.target.value)} - options={filterUnknown([ - { value: "disabled", label: "Disabled" }, - { value: "basic", label: "Basic" }, - { value: "all", label: "All" }, - ])} - /> - -
-
+
+ + setShowRenewLeaseConfirm(false)} 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." - variant="danger" - confirmText="Renew Lease" + description="Are you sure you want to renew the DHCP lease? This may temporarily disconnect the device." onConfirm={() => { - handleRenewLease(); setShowRenewLeaseConfirm(false); }} + onClose={() => setShowRenewLeaseConfirm(false)} /> ); diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts new file mode 100644 index 0000000..43ab7ec --- /dev/null +++ b/ui/src/utils/jsonrpc.ts @@ -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 { + 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; +}