From 8f488a3cb5efadd977900044765fab9e71b0b2fe Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 16 Apr 2025 21:09:23 +0200 Subject: [PATCH 1/4] feat(network): enhance network settings UI with domain management and improved layout - Added custom domain input and selection options for DHCP and local domains. - Improved layout for displaying network settings, including DHCP lease information and IPv6 addresses. - Refactored state management for network settings and added handlers for hostname and domain changes. - Updated the display of network settings to enhance user experience and accessibility. --- .../routes/devices.$id.settings.network.tsx | 717 ++++++++++++------ 1 file changed, 489 insertions(+), 228 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 59d52ef..968dc01 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -1,18 +1,28 @@ import { useCallback, useEffect, useState } from "react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; -import { SelectMenuBasic } from "../components/SelectMenuBasic"; -import { SettingsPageHeader } from "../components/SettingsPageheader"; - -import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores"; +import { + IPv4Mode, + IPv6Mode, + LLDPMode, + mDNSMode, + NetworkSettings, + NetworkState, + TimeSyncMode, + useNetworkStateStore, +} from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; -import { Button } from "@components/Button"; +import { Button, LinkButton } from "@components/Button"; import { GridCard } from "@components/Card"; import InputField from "@components/InputField"; -import { SettingsItem } from "./devices.$id.settings"; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; +import { SettingsPageHeader } from "../components/SettingsPageheader"; +import { SelectMenuBasic } from "../components/SelectMenuBasic"; + +import { SettingsItem } from "./devices.$id.settings"; +import Fieldset from "../components/Fieldset"; dayjs.extend(relativeTime); @@ -25,13 +35,9 @@ const defaultNetworkSettings: NetworkSettings = { lldp_tx_tlvs: [], mdns_mode: "unknown", time_sync_mode: "unknown", -} +}; export function LifeTimeLabel({ lifetime }: { lifetime: string }) { - if (lifetime == "") { - return N/A; - } - const [remaining, setRemaining] = useState(null); useEffect(() => { @@ -43,23 +49,48 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { return () => clearInterval(interval); }, [lifetime]); - return <> - {dayjs(lifetime).format()} - {remaining && <> - {" "} - ({remaining}) - - } - + return ( + <> + {dayjs(lifetime).format("YYYY-MM-DD HH:mm")} + {remaining && ( + <> + {" "} + + ({remaining}) + + + )} + + ); } export default function SettingsNetworkRoute() { const [send] = useJsonRpc(); - const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]); + const [networkState, setNetworkState] = useNetworkStateStore(state => [ + state, + state.setNetworkState, + ]); - const [networkSettings, setNetworkSettings] = useState(defaultNetworkSettings); + const [networkSettings, setNetworkSettings] = + useState(defaultNetworkSettings); const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); + 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 => { @@ -70,19 +101,25 @@ export default function SettingsNetworkRoute() { }); }, [send]); - const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => { - setNetworkSettingsLoaded(false); - send("setNetworkSettings", { settings }, resp => { - if ("error" in resp) { - notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message)); + const setNetworkSettingsRemote = useCallback( + (settings: NetworkSettings) => { + setNetworkSettingsLoaded(false); + send("setNetworkSettings", { settings }, resp => { + if ("error" in resp) { + notifications.error( + "Failed to save network settings: " + + (resp.error.data ? resp.error.data : resp.error.message), + ); + setNetworkSettingsLoaded(true); + return; + } + setNetworkSettings(resp.result as NetworkSettings); setNetworkSettingsLoaded(true); - return; - } - setNetworkSettings(resp.result as NetworkSettings); - setNetworkSettingsLoaded(true); - notifications.success("Network settings saved"); - }); - }, [send]); + notifications.success("Network settings saved"); + }); + }, + [send], + ); const getNetworkState = useCallback(() => { send("getNetworkState", {}, resp => { @@ -90,7 +127,7 @@ export default function SettingsNetworkRoute() { console.log(resp.result); setNetworkState(resp.result as NetworkState); }); - }, [send]); + }, [send, setNetworkState]); const handleRenewLease = useCallback(() => { send("renewDHCPLease", {}, resp => { @@ -116,7 +153,9 @@ export default function SettingsNetworkRoute() { }; const handleLldpModeChange = (value: LLDPMode | string) => { - setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode }); + const newSettings = { ...networkSettings, lldp_mode: value as LLDPMode }; + setNetworkSettings(newSettings); + setNetworkSettingsRemote(newSettings); }; // const handleLldpTxTlvsChange = (value: string[]) => { @@ -124,94 +163,175 @@ export default function SettingsNetworkRoute() { // }; const handleMdnsModeChange = (value: mDNSMode | string) => { - setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode }); + const newSettings = { ...networkSettings, mdns_mode: value as mDNSMode }; + setNetworkSettings(newSettings); + setNetworkSettingsRemote(newSettings); }; const handleTimeSyncModeChange = (value: TimeSyncMode | string) => { - setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode }); + const newSettings = { ...networkSettings, time_sync_mode: value as TimeSyncMode }; + setNetworkSettings(newSettings); + setNetworkSettingsRemote(newSettings); }; - const filterUnknown = useCallback((options: { value: string; label: string; }[]) => { - if (!networkSettingsLoaded) return options; - return options.filter(option => option.value !== "unknown"); - }, [networkSettingsLoaded]); + const handleHostnameChange = (value: string) => { + if (value === networkSettings.hostname) return; + const newSettings = { ...networkSettings, hostname: value }; + setNetworkSettings(newSettings); + setNetworkSettingsRemote(newSettings); + }; + + const handleDomainChange = (value: string) => { + if (value === networkSettings.domain) return; + const newSettings = { ...networkSettings, domain: value }; + setNetworkSettings(newSettings); + setNetworkSettingsRemote(newSettings); + }; + + 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], + ); return ( -
- +
+
} + description="Hardware identifier for the network interface" > - - {networkState?.mac_address} - +
- Hostname for the device -
- - Leave blank for default - - - } + description="Device identifier on the network. Blank for system default" > - { - setNetworkSettings({ ...networkSettings, hostname: e.target.value }); - }} - disabled={!networkSettingsLoaded} - /> +
+
+ { + handleHostnameChange(e.target.value); + }} + /> +
+
+
- - Domain for the device -
- - Leave blank to use DHCP provided domain, if there is no domain, use local - - - } - > - { - setNetworkSettings({ ...networkSettings, domain: e.target.value }); - }} - disabled={!networkSettingsLoaded} - /> -
+
+ +
+ handleDomainOptionChange(e.target.value)} + options={[ + { value: "dhcp", label: "DHCP provided" }, + { value: "local", label: ".local" }, + { value: "custom", label: "Custom" }, + ]} + /> +
+
+ {selectedDomainOption === "custom" && ( +
+ setCustomDomain(e.target.value)} + /> +
+ )} +
+
+ + handleMdnsModeChange(e.target.value)} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "auto", label: "Auto" }, + { value: "ipv4_only", label: "IPv4 only" }, + { value: "ipv6_only", label: "IPv6 only" }, + ])} + /> + +
+
+ + { + handleTimeSyncModeChange(e.target.value); + }} + options={filterUnknown([ + { value: "unknown", label: "..." }, + // { value: "auto", label: "Auto" }, + { value: "ntp_only", label: "NTP only" }, + { value: "ntp_and_http", label: "NTP and HTTP" }, + { value: "http_only", label: "HTTP only" }, + // { value: "custom", label: "Custom" }, + ])} + /> + +
+ +
+
- + handleIpv4ModeChange(e.target.value)} - disabled={!networkSettingsLoaded} options={filterUnknown([ { value: "dhcp", label: "DHCP" }, // { value: "static", label: "Static" }, @@ -220,44 +340,199 @@ export default function SettingsNetworkRoute() { {networkState?.dhcp_lease && ( -
-
-
-

- Current DHCP Lease -

-
-
    - {networkState?.dhcp_lease?.ip &&
  • IP: {networkState?.dhcp_lease?.ip}
  • } - {networkState?.dhcp_lease?.netmask &&
  • Subnet: {networkState?.dhcp_lease?.netmask}
  • } - {networkState?.dhcp_lease?.broadcast &&
  • Broadcast: {networkState?.dhcp_lease?.broadcast}
  • } - {networkState?.dhcp_lease?.ttl &&
  • TTL: {networkState?.dhcp_lease?.ttl}
  • } - {networkState?.dhcp_lease?.mtu &&
  • MTU: {networkState?.dhcp_lease?.mtu}
  • } - {networkState?.dhcp_lease?.hostname &&
  • Hostname: {networkState?.dhcp_lease?.hostname}
  • } - {networkState?.dhcp_lease?.domain &&
  • Domain: {networkState?.dhcp_lease?.domain}
  • } - {networkState?.dhcp_lease?.routers &&
  • Gateway: {networkState?.dhcp_lease?.routers.join(", ")}
  • } - {networkState?.dhcp_lease?.dns &&
  • DNS: {networkState?.dhcp_lease?.dns.join(", ")}
  • } - {networkState?.dhcp_lease?.ntp_servers &&
  • NTP Servers: {networkState?.dhcp_lease?.ntp_servers.join(", ")}
  • } - {networkState?.dhcp_lease?.server_id &&
  • Server ID: {networkState?.dhcp_lease?.server_id}
  • } - {networkState?.dhcp_lease?.bootp_next_server &&
  • BootP Next Server: {networkState?.dhcp_lease?.bootp_next_server}
  • } - {networkState?.dhcp_lease?.bootp_server_name &&
  • BootP Server Name: {networkState?.dhcp_lease?.bootp_server_name}
  • } - {networkState?.dhcp_lease?.bootp_file &&
  • Boot File: {networkState?.dhcp_lease?.bootp_file}
  • } - {networkState?.dhcp_lease?.lease_expiry &&
  • - Lease Expiry: -
  • } - {/* {JSON.stringify(networkState?.dhcp_lease)} */} -
+
+
+

+ Current DHCP Lease +

+ +
+
+ {networkState?.dhcp_lease?.ip && ( +
+ + IP Address + + + {networkState?.dhcp_lease?.ip} + +
+ )} + + {networkState?.dhcp_lease?.netmask && ( +
+ + Subnet Mask + + + {networkState?.dhcp_lease?.netmask} + +
+ )} + + {networkState?.dhcp_lease?.dns && ( +
+ + DNS Servers + + + {networkState?.dhcp_lease?.dns.map(dns => ( +
{dns}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.broadcast && ( +
+ + Broadcast + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + + {networkState?.dhcp_lease?.domain && ( +
+ + Domain + + + {networkState?.dhcp_lease?.domain} + +
+ )} + + {networkState?.dhcp_lease?.ntp_servers && + networkState?.dhcp_lease?.ntp_servers.length > 0 && ( +
+
+ NTP Servers +
+
+ {networkState?.dhcp_lease?.ntp_servers.map(server => ( +
{server}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.hostname && ( +
+ + Hostname + + + {networkState?.dhcp_lease?.hostname} + +
+ )} +
+ +
+ {networkState?.dhcp_lease?.routers && + networkState?.dhcp_lease?.routers.length > 0 && ( +
+ + Gateway + + + {networkState?.dhcp_lease?.routers.map(router => ( +
{router}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.server_id && ( +
+ + DHCP Server + + + {networkState?.dhcp_lease?.server_id} + +
+ )} + + {networkState?.dhcp_lease?.lease_expiry && ( +
+ + Lease Expires + + + + +
+ )} + + {networkState?.dhcp_lease?.mtu && ( +
+ + MTU + + + {networkState?.dhcp_lease?.mtu} + +
+ )} + + {networkState?.dhcp_lease?.ttl && ( +
+ + TTL + + + {networkState?.dhcp_lease?.ttl} + +
+ )} + + {networkState?.dhcp_lease?.bootp_next_server && ( +
+ + Boot Next Server + + + {networkState?.dhcp_lease?.bootp_next_server} + +
+ )} + + {networkState?.dhcp_lease?.bootp_server_name && ( +
+ + Boot Server Name + + + {networkState?.dhcp_lease?.bootp_server_name} + +
+ )} + + {networkState?.dhcp_lease?.bootp_file && ( +
+ + Boot File + + + {networkState?.dhcp_lease?.bootp_file} + +
+ )}
-
+
@@ -266,15 +541,11 @@ export default function SettingsNetworkRoute() { )}
- + handleIpv6ModeChange(e.target.value)} - disabled={!networkSettingsLoaded} options={filterUnknown([ // { value: "disabled", label: "Disabled" }, { value: "slaac", label: "SLAAC" }, @@ -287,55 +558,96 @@ export default function SettingsNetworkRoute() { {networkState?.ipv6_addresses && ( -
-
-
-

- IPv6 Information -

-
-
-

- IPv6 Link-local -

-

+

+
+

+ IPv6 Information +

+ +
+ {networkState?.dhcp_lease?.ip && ( +
+ + Link-local + + {networkState?.ipv6_link_local} -

+
-
-

- IPv6 Addresses -

-
    - {networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => ( -
  • - {addr.address} - {addr.valid_lifetime && <> -
    - - valid_lft: {" "} - - - - } - {addr.preferred_lifetime && <> -
    - - pref_lft: {" "} - - - - } -
  • + )} +
+ +
+ {networkState?.ipv6_addresses && + networkState?.ipv6_addresses.length > 0 && ( +
+

IPv6 Addresses

+ {[ + ...networkState.ipv6_addresses, + ...networkState.ipv6_addresses, + ].map(addr => ( +
+
+
+ + Address + + + {addr.address} + +
+ + {addr.valid_lifetime && ( +
+ + Valid Lifetime + + + {addr.valid_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} + {addr.preferred_lifetime && ( +
+ + Preferred Lifetime + + + {addr.preferred_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} +
+
))} - -
-
+
+ )}
)}
-
+
handleLldpModeChange(e.target.value)} - disabled={!networkSettingsLoaded} options={filterUnknown([ { value: "disabled", label: "Disabled" }, { value: "basic", label: "Basic" }, @@ -353,56 +664,6 @@ export default function SettingsNetworkRoute() { />
-
- - handleMdnsModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "disabled", label: "Disabled" }, - { value: "auto", label: "Auto" }, - { value: "ipv4_only", label: "IPv4 only" }, - { value: "ipv6_only", label: "IPv6 only" }, - ])} - /> - -
-
- - handleTimeSyncModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "unknown", label: "..." }, - // { value: "auto", label: "Auto" }, - { value: "ntp_only", label: "NTP only" }, - { value: "ntp_and_http", label: "NTP and HTTP" }, - { value: "http_only", label: "HTTP only" }, - // { value: "custom", label: "Custom" }, - ])} - /> - -
-
-
-
+
); } From fcf2dbab60f9c2b03d21c795219cd4268e9d9bfa Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 16 Apr 2025 21:44:33 +0200 Subject: [PATCH 2/4] Re-add save button --- .../routes/devices.$id.settings.network.tsx | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 968dc01..d2e8703 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; @@ -73,6 +73,10 @@ export default function SettingsNetworkRoute() { const [networkSettings, setNetworkSettings] = useState(defaultNetworkSettings); + + // We use this to determine whether the settings have changed + const firstNetworkSettings = useRef(); + const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); const [customDomain, setCustomDomain] = useState(""); @@ -97,6 +101,10 @@ export default function SettingsNetworkRoute() { 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]); @@ -113,6 +121,8 @@ export default function SettingsNetworkRoute() { 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); setNetworkSettingsLoaded(true); notifications.success("Network settings saved"); @@ -153,9 +163,7 @@ export default function SettingsNetworkRoute() { }; const handleLldpModeChange = (value: LLDPMode | string) => { - const newSettings = { ...networkSettings, lldp_mode: value as LLDPMode }; - setNetworkSettings(newSettings); - setNetworkSettingsRemote(newSettings); + setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode }); }; // const handleLldpTxTlvsChange = (value: string[]) => { @@ -163,29 +171,19 @@ export default function SettingsNetworkRoute() { // }; const handleMdnsModeChange = (value: mDNSMode | string) => { - const newSettings = { ...networkSettings, mdns_mode: value as mDNSMode }; - setNetworkSettings(newSettings); - setNetworkSettingsRemote(newSettings); + setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode }); }; const handleTimeSyncModeChange = (value: TimeSyncMode | string) => { - const newSettings = { ...networkSettings, time_sync_mode: value as TimeSyncMode }; - setNetworkSettings(newSettings); - setNetworkSettingsRemote(newSettings); + setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode }); }; const handleHostnameChange = (value: string) => { - if (value === networkSettings.hostname) return; - const newSettings = { ...networkSettings, hostname: value }; - setNetworkSettings(newSettings); - setNetworkSettingsRemote(newSettings); + setNetworkSettings({ ...networkSettings, hostname: value }); }; const handleDomainChange = (value: string) => { - if (value === networkSettings.domain) return; - const newSettings = { ...networkSettings, domain: value }; - setNetworkSettings(newSettings); - setNetworkSettingsRemote(newSettings); + setNetworkSettings({ ...networkSettings, domain: value }); }; const handleDomainOptionChange = (value: string) => { @@ -208,6 +206,9 @@ export default function SettingsNetworkRoute() { [networkSettingsLoaded], ); + console.log("firstNetworkSettings", firstNetworkSettings.current); + console.log("networkSettings", networkSettings); + return (
@@ -239,7 +240,7 @@ export default function SettingsNetworkRoute() { type="text" placeholder="jetkvm" defaultValue={networkSettings.hostname} - onBlur={e => { + onChange={e => { handleHostnameChange(e.target.value); }} /> @@ -300,6 +301,7 @@ export default function SettingsNetworkRoute() { />
+
+ +