From 8e2ed6059d17afe3f534502e383197d9132c273a Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 19 Mar 2025 15:57:53 +0100 Subject: [PATCH] Refactor: remove USB configuration components and update settings structure (#271) --- ui/src/components/USBConfigDialog.tsx | 117 --------- ui/src/components/UsbDeviceSetting.tsx | 233 +++++++++++++----- ...sbConfigSetting.tsx => UsbInfoSetting.tsx} | 158 ++++++++++-- .../routes/devices.$id.settings.hardware.tsx | 6 +- ui/src/routes/devices.$id.settings.tsx | 10 +- 5 files changed, 326 insertions(+), 198 deletions(-) delete mode 100644 ui/src/components/USBConfigDialog.tsx rename ui/src/components/{UsbConfigSetting.tsx => UsbInfoSetting.tsx} (51%) diff --git a/ui/src/components/USBConfigDialog.tsx b/ui/src/components/USBConfigDialog.tsx deleted file mode 100644 index db8b677..0000000 --- a/ui/src/components/USBConfigDialog.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Button } from "@components/Button"; -import { InputFieldWithLabel } from "./InputField"; -import { UsbConfigState } from "@/hooks/stores"; -import { useEffect, useCallback, useState } from "react"; -import { useJsonRpc } from "../hooks/useJsonRpc"; -import { USBConfig } from "./UsbConfigSetting"; - -export default function UpdateUsbConfigModal({ - onSetUsbConfig, - onRestoreToDefault, -}: { - onSetUsbConfig: (usbConfig: USBConfig) => void; - onRestoreToDefault: () => void; -}) { - const [usbConfigState, setUsbConfigState] = useState({ - vendor_id: "", - product_id: "", - serial_number: "", - manufacturer: "", - product: "", - }); - - const [send] = useJsonRpc(); - - const syncUsbConfig = useCallback(() => { - send("getUsbConfig", {}, resp => { - if ("error" in resp) { - console.error("Failed to load USB Config:", resp.error); - } else { - setUsbConfigState(resp.result as UsbConfigState); - } - }); - }, [send, setUsbConfigState]); - - // Load stored usb config from the backend - useEffect(() => { - syncUsbConfig(); - }, [syncUsbConfig]); - - const handleUsbVendorIdChange = (value: string) => { - setUsbConfigState({ ...usbConfigState, vendor_id: value }); - }; - - const handleUsbProductIdChange = (value: string) => { - setUsbConfigState({ ...usbConfigState, product_id: value }); - }; - - const handleUsbSerialChange = (value: string) => { - setUsbConfigState({ ...usbConfigState, serial_number: value }); - }; - - const handleUsbManufacturer = (value: string) => { - setUsbConfigState({ ...usbConfigState, manufacturer: value }); - }; - - const handleUsbProduct = (value: string) => { - setUsbConfigState({ ...usbConfigState, product: value }); - }; - - return ( -
-
- handleUsbVendorIdChange(e.target.value)} - /> - handleUsbProductIdChange(e.target.value)} - /> - handleUsbSerialChange(e.target.value)} - /> - handleUsbManufacturer(e.target.value)} - /> - handleUsbProduct(e.target.value)} - /> -
-
-
-
- ); -} diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx index 1c8812c..605ae4d 100644 --- a/ui/src/components/UsbDeviceSetting.tsx +++ b/ui/src/components/UsbDeviceSetting.tsx @@ -5,6 +5,10 @@ import { useJsonRpc } from "../hooks/useJsonRpc"; import notifications from "../notifications"; import { SettingsItem } from "../routes/devices.$id.settings"; import Checkbox from "./Checkbox"; +import { Button } from "./Button"; +import { SelectMenuBasic } from "./SelectMenuBasic"; +import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import Fieldset from "./Fieldset"; export interface USBConfig { vendor_id: string; @@ -26,12 +30,43 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = { absolute_mouse: true, relative_mouse: true, mass_storage: true, -} +}; + +const usbPresets = [ + { + label: "Keyboard, Mouse and Mass Storage", + value: "default", + config: { + keyboard: true, + absolute_mouse: true, + relative_mouse: true, + mass_storage: true, + }, + }, + { + label: "Keyboard Only", + value: "keyboard_only", + config: { + keyboard: true, + absolute_mouse: false, + relative_mouse: false, + mass_storage: false, + }, + }, + { + label: "Custom", + value: "custom", + }, +]; export function UsbDeviceSetting() { const [send] = useJsonRpc(); + const [loading, setLoading] = useState(false); + + const [usbDeviceConfig, setUsbDeviceConfig] = + useState(defaultUsbDeviceConfig); + const [selectedPreset, setSelectedPreset] = useState("default"); - const [usbDeviceConfig, setUsbDeviceConfig] = useState(defaultUsbDeviceConfig); const syncUsbDeviceConfig = useCallback(() => { send("getUsbDevices", {}, resp => { if ("error" in resp) { @@ -40,90 +75,168 @@ export function UsbDeviceSetting() { `Failed to load USB devices: ${resp.error.data || "Unknown error"}`, ); } else { - console.log("syncUsbDeviceConfig#getUsbDevices result:", resp.result); const usbConfigState = resp.result as UsbDeviceConfig; setUsbDeviceConfig(usbConfigState); + + // Set the appropriate preset based on current config + const matchingPreset = usbPresets.find( + preset => + preset.value !== "custom" && + preset.config && + Object.keys(preset.config).length === Object.keys(usbConfigState).length && + Object.keys(preset.config).every(key => { + const configKey = key as keyof typeof preset.config; + return preset.config[configKey] === usbConfigState[configKey]; + }), + ); + + setSelectedPreset(matchingPreset ? matchingPreset.value : "custom"); } }); }, [send]); const handleUsbConfigChange = useCallback( (devices: UsbDeviceConfig) => { - send("setUsbDevices", { devices }, resp => { + setLoading(true); + send("setUsbDevices", { devices }, async resp => { if ("error" in resp) { notifications.error( `Failed to set usb devices: ${resp.error.data || "Unknown error"}`, ); + setLoading(false); return; } - notifications.success( - `USB Devices updated` - ); + + // We need some time to ensure the USB devices are updated + await new Promise(resolve => setTimeout(resolve, 2000)); + setLoading(false); syncUsbDeviceConfig(); + notifications.success(`USB Devices updated`); }); }, [send, syncUsbDeviceConfig], ); - const onUsbConfigItemChange = useCallback((key: keyof UsbDeviceConfig) => (e: React.ChangeEvent) => { - setUsbDeviceConfig((val) => { - val[key] = e.target.checked; - handleUsbConfigChange(val); - return val; - }); - }, [handleUsbConfigChange]); + const onUsbConfigItemChange = useCallback( + (key: keyof UsbDeviceConfig) => (e: React.ChangeEvent) => { + setUsbDeviceConfig(val => { + val[key] = e.target.checked; + handleUsbConfigChange(val); + return val; + }); + }, + [handleUsbConfigChange], + ); + + const handlePresetChange = useCallback( + async (e: React.ChangeEvent) => { + const newPreset = e.target.value; + setSelectedPreset(newPreset); + + if (newPreset !== "custom") { + const presetConfig = usbPresets.find( + preset => preset.value === newPreset, + )?.config; + + if (presetConfig) { + handleUsbConfigChange(presetConfig); + } + } + }, + [handleUsbConfigChange], + ); useEffect(() => { syncUsbDeviceConfig(); }, [syncUsbDeviceConfig]); return ( - <> +
-
- - - -
-
- - - -
-
- - - -
-
- - - -
- + + + + + + + + {selectedPreset === "custom" && ( +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+ )} +
); } diff --git a/ui/src/components/UsbConfigSetting.tsx b/ui/src/components/UsbInfoSetting.tsx similarity index 51% rename from ui/src/components/UsbConfigSetting.tsx rename to ui/src/components/UsbInfoSetting.tsx index 1e8ab03..4ac93ff 100644 --- a/ui/src/components/UsbConfigSetting.tsx +++ b/ui/src/components/UsbInfoSetting.tsx @@ -1,6 +1,8 @@ import { useMemo } from "react"; import { useCallback } from "react"; +import { Button } from "@components/Button"; +import { InputFieldWithLabel } from "./InputField"; import { useEffect, useState } from "react"; import { UsbConfigState } from "../hooks/stores"; @@ -8,7 +10,7 @@ import { useJsonRpc } from "../hooks/useJsonRpc"; import notifications from "../notifications"; import { SettingsItem } from "../routes/devices.$id.settings"; import { SelectMenuBasic } from "./SelectMenuBasic"; -import USBConfigDialog from "./USBConfigDialog"; +import Fieldset from "./Fieldset"; const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&"); @@ -51,8 +53,9 @@ const usbConfigs = [ type UsbConfigMap = Record; -export function UsbConfigSetting() { +export function UsbInfoSetting() { const [send] = useJsonRpc(); + const [loading, setLoading] = useState(false); const [usbConfigProduct, setUsbConfigProduct] = useState(""); const [deviceId, setDeviceId] = useState(""); @@ -110,17 +113,23 @@ export function UsbConfigSetting() { const handleUsbConfigChange = useCallback( (usbConfig: USBConfig) => { - send("setUsbConfig", { usbConfig }, resp => { + setLoading(true); + send("setUsbConfig", { usbConfig }, async resp => { if ("error" in resp) { notifications.error( `Failed to set usb config: ${resp.error.data || "Unknown error"}`, ); + setLoading(false); return; } - // setUsbConfigProduct(usbConfig.product); + + // We need some time to ensure the USB devices are updated + await new Promise(resolve => setTimeout(resolve, 2000)); + setLoading(false); notifications.success( `USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`, ); + syncUsbConfigProduct(); }); }, @@ -141,18 +150,18 @@ export function UsbConfigSetting() { }, [send, syncUsbConfigProduct]); return ( - <> -
- +
{ if (e.target.value === "custom") { setUsbConfigProduct(e.target.value); @@ -165,13 +174,130 @@ export function UsbConfigSetting() { /> {usbConfigProduct === "custom" && ( - handleUsbConfigChange(usbConfig)} - onRestoreToDefault={() => - handleUsbConfigChange(usbConfigData[usbConfigs[0].value]) - } - /> +
+ handleUsbConfigChange(usbConfig)} + onRestoreToDefault={() => + handleUsbConfigChange(usbConfigData[usbConfigs[0].value]) + } + /> +
)} - +
+ ); +} + +function USBConfigDialog({ + loading, + onSetUsbConfig, + onRestoreToDefault, +}: { + loading: boolean; + onSetUsbConfig: (usbConfig: USBConfig) => void; + onRestoreToDefault: () => void; +}) { + const [usbConfigState, setUsbConfigState] = useState({ + vendor_id: "", + product_id: "", + serial_number: "", + manufacturer: "", + product: "", + }); + + const [send] = useJsonRpc(); + + const syncUsbConfig = useCallback(() => { + send("getUsbConfig", {}, resp => { + if ("error" in resp) { + console.error("Failed to load USB Config:", resp.error); + } else { + setUsbConfigState(resp.result as UsbConfigState); + } + }); + }, [send, setUsbConfigState]); + + // Load stored usb config from the backend + useEffect(() => { + syncUsbConfig(); + }, [syncUsbConfig]); + + const handleUsbVendorIdChange = (value: string) => { + setUsbConfigState({ ...usbConfigState, vendor_id: value }); + }; + + const handleUsbProductIdChange = (value: string) => { + setUsbConfigState({ ...usbConfigState, product_id: value }); + }; + + const handleUsbSerialChange = (value: string) => { + setUsbConfigState({ ...usbConfigState, serial_number: value }); + }; + + const handleUsbManufacturer = (value: string) => { + setUsbConfigState({ ...usbConfigState, manufacturer: value }); + }; + + const handleUsbProduct = (value: string) => { + setUsbConfigState({ ...usbConfigState, product: value }); + }; + + return ( +
+
+ handleUsbVendorIdChange(e.target.value)} + /> + handleUsbProductIdChange(e.target.value)} + /> + handleUsbSerialChange(e.target.value)} + /> + handleUsbManufacturer(e.target.value)} + /> + handleUsbProduct(e.target.value)} + /> +
+
+
+
); } diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 3a60466..d9d3919 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -6,7 +6,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "../notifications"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; -import { UsbConfigSetting } from "../components/UsbConfigSetting"; +import { UsbInfoSetting } from "../components/UsbInfoSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { FeatureFlag } from "../components/FeatureFlag"; @@ -131,11 +131,11 @@ export default function SettingsHardwareRoute() {
- + - + ); diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 6084afb..1a8de03 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -16,6 +16,7 @@ import { cx } from "../cva.config"; import { useUiStore } from "../hooks/stores"; import useKeyboard from "../hooks/useKeyboard"; import { useResizeObserver } from "../hooks/useResizeObserver"; +import LoadingSpinner from "../components/LoadingSpinner"; /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ export default function SettingsRoute() { @@ -206,7 +207,7 @@ export default function SettingsRoute() { -
+
{/* */}
-

{title}

+
+

{title}

+ {loading && } +

{description}

{children ?
{children}
: null}