From fd3a8cb5534692c8030ab56e3952e7ed8029906d Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Sun, 13 Apr 2025 03:53:20 +0200 Subject: [PATCH] feat(ui): add network settings tab --- internal/udhcpc/udhcpc.go | 4 + jsonrpc.go | 2 + network.go | 29 ++++ ui/src/main.tsx | 5 + .../routes/devices.$id.settings.network.tsx | 135 ++++++++++++++++++ ui/src/routes/devices.$id.settings.tsx | 12 ++ 6 files changed, 187 insertions(+) create mode 100644 ui/src/routes/devices.$id.settings.network.tsx diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go index 1459b40..298ac26 100644 --- a/internal/udhcpc/udhcpc.go +++ b/internal/udhcpc/udhcpc.go @@ -143,3 +143,7 @@ func (c *DHCPClient) loadLeaseFile() error { return nil } + +func (c *DHCPClient) GetLease() *Lease { + return c.lease +} diff --git a/jsonrpc.go b/jsonrpc.go index 0c7d7fd..3e28054 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -960,6 +960,8 @@ var rpcHandlers = map[string]RPCHandler{ "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, + "getNetworkState": {Func: rpcGetNetworkState}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, diff --git a/network.go b/network.go index b2588c9..fd62dd8 100644 --- a/network.go +++ b/network.go @@ -47,6 +47,14 @@ type NetworkInterfaceState struct { checked bool } +type RpcNetworkState struct { + InterfaceName string `json:"interface_name"` + MacAddress string `json:"mac_address"` + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` + DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"` +} + func (s *NetworkInterfaceState) IsUp() bool { return s.interfaceUp } @@ -305,6 +313,27 @@ func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) { } } +func rpcGetNetworkState() RpcNetworkState { + return RpcNetworkState{ + InterfaceName: networkState.interfaceName, + MacAddress: networkState.macAddr.String(), + IPv4: networkState.ipv4Addr.String(), + IPv6: networkState.ipv6Addr.String(), + DHCPLease: networkState.dhcpClient.GetLease(), + } +} + +func rpcRenewDHCPLease() error { + if networkState == nil { + return fmt.Errorf("network state not initialized") + } + if networkState.dhcpClient == nil { + return fmt.Errorf("dhcp client not initialized") + } + + return networkState.dhcpClient.Renew() +} + func initNetwork() { ensureConfigLoaded() diff --git a/ui/src/main.tsx b/ui/src/main.tsx index e09a2a9..f4bdd34 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -42,6 +42,7 @@ import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; +import SettingsNetworkRoute from "./routes/devices.$id.settings.network"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; import SettingsMacrosRoute from "./routes/devices.$id.settings.macros"; import SettingsMacrosAddRoute from "./routes/devices.$id.settings.macros.add"; @@ -156,6 +157,10 @@ if (isOnDevice) { path: "hardware", element: , }, + { + path: "network", + element: , + }, { path: "access", children: [ diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx new file mode 100644 index 0000000..c7ade5f --- /dev/null +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useState } from "react"; + +import { SettingsPageHeader } from "../components/SettingsPageheader"; +import { SelectMenuBasic } from "../components/SelectMenuBasic"; + +import { SettingsItem } from "./devices.$id.settings"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { Button } from "@components/Button"; +import notifications from "@/notifications"; + +interface DhcpLease { + ip?: string; + netmask?: string; + broadcast?: string; + ttl?: string; + mtu?: string; + hostname?: string; + domain?: string; + bootp_next_server?: string; + bootp_server_name?: string; + bootp_file?: string; + timezone?: string; + routers?: string[]; + dns?: string[]; + ntp_servers?: string[]; + lpr_servers?: string[]; + _time_servers?: string[]; + _name_servers?: string[]; + _log_servers?: string[]; + _cookie_servers?: string[]; + _wins_servers?: string[]; + _swap_server?: string; + boot_size?: string; + root_path?: string; + lease?: string; + dhcp_type?: string; + server_id?: string; + message?: string; + tftp?: string; + bootfile?: string; +} + + +interface NetworkState { + interface_name?: string; + mac_address?: string; + ipv4?: string; + ipv6?: string; + dhcp_lease?: DhcpLease; +} + +export default function SettingsNetworkRoute() { + const [send] = useJsonRpc(); + const [networkState, setNetworkState] = useState(null); + + + const getNetworkState = useCallback(() => { + send("getNetworkState", {}, resp => { + if ("error" in resp) return; + setNetworkState(resp.result as NetworkState); + }); + }, [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"); + getNetworkState(); + } + }); + }, [send, getNetworkState]); + + useEffect(() => { + getNetworkState(); + }, [getNetworkState]); + + return ( +
+ +
+ {networkState?.ipv4} + } + /> +
+
+ {networkState?.ipv6}} + /> +
+
+ {networkState?.mac_address}} + /> +
+
+ +
    + {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(", ")}
  • } +
+ } + > +
+
+ ); +} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index c0b4181..f8e5262 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -9,6 +9,7 @@ import { LuArrowLeft, LuPalette, LuCommand, + LuNetwork, } from "react-icons/lu"; import React, { useEffect, useRef, useState } from "react"; @@ -207,6 +208,17 @@ export default function SettingsRoute() { +
+ (isActive ? "active" : "")} + > +
+ +

Network

+
+
+