diff --git a/cloud.go b/cloud.go index db47727..c2cd554 100644 --- a/cloud.go +++ b/cloud.go @@ -5,15 +5,15 @@ import ( "context" "encoding/json" "fmt" + "github.com/coder/websocket/wsjson" "net/http" "net/url" - "github.com/coder/websocket/wsjson" "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/gin-gonic/gin" "github.com/coder/websocket" + "github.com/gin-gonic/gin" ) type CloudRegisterRequest struct { diff --git a/ui/src/components/UsbConfigDialog.tsx b/ui/src/components/UsbConfigDialog.tsx new file mode 100644 index 0000000..3ea5fd6 --- /dev/null +++ b/ui/src/components/UsbConfigDialog.tsx @@ -0,0 +1,187 @@ +import { GridCard } from "@/components/Card"; +import {useCallback, useState} from "react"; +import { Button } from "@components/Button"; +import LogoBlueIcon from "@/assets/logo-blue.svg"; +import LogoWhiteIcon from "@/assets/logo-white.svg"; +import Modal from "@components/Modal"; +import { InputFieldWithLabel } from "./InputField"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { useUsbConfigModalStore } from "@/hooks/stores"; + +export default function UsbConfigDialog({ + open, + setOpen, +}: { + open: boolean; + setOpen: (open: boolean) => void; +}) { + return ( + setOpen(false)}> + + + ); +} + +export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { + const { modalView, setModalView } = useUsbConfigModalStore(); + const [error, setError] = useState(null); + + const [send] = useJsonRpc(); + + const handleUsbConfigChange = useCallback((usbConfig: object) => { + send("setUsbConfig", { usbConfig }, resp => { + if ("error" in resp) { + setError(`Failed to update USB Config: ${resp.error.data || "Unknown error"}`,); + return; + } + setModalView("updateUsbConfigSuccess"); + }); + }, [send, setModalView]); + + return ( + +
+ {modalView === "updateUsbConfig" && ( + setOpen(false)} + error={error} + /> + )} + + {modalView === "updateUsbConfigSuccess" && ( + setOpen(false)} + /> + )} +
+
+ ); +} + +function UpdateUsbConfigModal({ + onSetUsbConfig, + onCancel, + error, +}: { + onSetUsbConfig: (usb_config: object) => void; + onCancel: () => void; + error: string | null; +}) { + const [usbConfig, setUsbConfig] = useState({ + usb_vendor_id: '', + usb_product_id: '', + usb_serial_number: '', + usb_manufacturer: '', + usb_name: '', + }) + const handleUsbVendorIdChange = (vendorId: string) => { + setUsbConfig({... usbConfig, usb_vendor_id: vendorId}) + }; + + const handleUsbProductIdChange = (productId: string) => { + setUsbConfig({... usbConfig, usb_product_id: productId}) + }; + + const handleUsbSerialChange = (serialNumber: string) => { + setUsbConfig({... usbConfig, usb_serial_number: serialNumber}) + }; + + const handleUsbManufacturer = (manufacturer: string) => { + setUsbConfig({... usbConfig, usb_manufacturer: manufacturer}) + }; + + const handleUsbName = (name: string) => { + setUsbConfig({... usbConfig, usb_name: name}) + }; + + return ( +
+
+ + +
+
+
+

USB Emulation Configuration

+

+ Set custom USB parameters to control the device USB emulation. The device will rebind once the parameters are updated. +

+
+ handleUsbVendorIdChange(e.target.value)} + /> + handleUsbProductIdChange(e.target.value)} + /> + handleUsbSerialChange(e.target.value)} + /> + handleUsbManufacturer(e.target.value)} + /> + handleUsbName(e.target.value)} + /> +
+
+ {error &&

{error}

} +
+
+ ); +} + +function SuccessModal({ + headline, + description, + onClose, +}: { + headline: string; + description: string; + onClose: () => void; +}) { + return ( +
+
+ + +
+
+
+

{headline}

+

{description}

+
+
+
+ ); +} diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index 12b8c3e..e0cedd4 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -3,14 +3,13 @@ import { useLocalAuthModalStore, useSettingsStore, useUiStore, - useUpdateStore, + useUpdateStore, useUsbConfigModalStore, } from "@/hooks/stores"; import { Checkbox } from "@components/Checkbox"; import { Button, LinkButton } from "@components/Button"; import { TextAreaWithLabel } from "@components/TextArea"; import { SectionHeader } from "@components/SectionHeader"; import { GridCard } from "@components/Card"; -import { InputFieldWithLabel } from "@components/InputField"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { cx } from "@/cva.config"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -26,6 +25,7 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog"; import { LocalDevice } from "@routes/devices.$id"; import { useRevalidator } from "react-router-dom"; import { ShieldCheckIcon } from "@heroicons/react/20/solid"; +import UsbConfigDialog from "@components/UsbConfigDialog"; export function SettingsItem({ title, @@ -110,14 +110,6 @@ export default function SettingsSidebar() { }); }, [send]); - const [usbConfig, setUsbConfig] = useState({ - usb_product_id: '', - usb_vendor_id: '', - usb_serial_number: '', - usb_manufacturer: '', - usb_name: '', - }) - const handleUsbEmulationToggle = useCallback( (enabled: boolean) => { send("setUsbEmulationState", { enabled: enabled }, resp => { @@ -215,17 +207,6 @@ export default function SettingsSidebar() { }); }; - const handleUsbConfigChange = useCallback((usbConfig: object) => { - send("setUsbConfig", { usbConfig }, resp => { - if ("error" in resp) { - notifications.error( - `Failed to update USB Config: ${resp.error.data || "Unknown error"}`, - ); - return; - } - }); - }, [send]); - const handleSSHKeyChange = (newKey: string) => { setSSHKey(newKey); }; @@ -260,26 +241,6 @@ export default function SettingsSidebar() { }); }, [send, sshKey]); - const handleUsbProductIdChange = (productId: string) => { - setUsbConfig({... usbConfig, usb_product_id: productId}) - }; - - const handleUsbVendorIdChange = (vendorId: string) => { - setUsbConfig({... usbConfig, usb_vendor_id: vendorId}) - }; - - const handleUsbSerialChange = (serialNumber: string) => { - setUsbConfig({... usbConfig, usb_serial_number: serialNumber}) - }; - - const handleUsbName = (name: string) => { - setUsbConfig({... usbConfig, usb_name: name}) - }; - - const handleUsbManufacturer = (manufacturer: string) => { - setUsbConfig({... usbConfig, usb_manufacturer: manufacturer}) - }; - const { setIsUpdateDialogOpen, setModalView, otaState } = useUpdateStore(); const handleCheckForUpdates = () => { if (otaState.updating) { @@ -380,7 +341,9 @@ export default function SettingsSidebar() { }, []); const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore(); + const { setModalView: setUsbConfigModalView } = useUsbConfigModalStore(); const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false); + const [isUsbConfigDialogOpen, setIsUsbConfigDialogOpen] = useState(false); useEffect(() => { if (isOnDevice) getDevice(); @@ -394,6 +357,14 @@ export default function SettingsSidebar() { } }, [getDevice, isLocalAuthDialogOpen]); + useEffect(() => { + if (!isOnDevice) return; + // Refresh device status when the local usb config dialog is closed + if (!isUsbConfigDialogOpen) { + getDevice(); + } + }, [getDevice, isUsbConfigDialogOpen]); + const revalidator = useRevalidator(); const [currentTheme, setCurrentTheme] = useState(() => { @@ -877,53 +848,20 @@ export default function SettingsSidebar() { )} {settings.developerMode && ( -
- handleUsbVendorIdChange(e.target.value)} - placeholder="Enter USB Vendor Id" - /> - handleUsbProductIdChange(e.target.value)} - placeholder="Enter USB Product Id" - /> - handleUsbSerialChange(e.target.value)} - placeholder="Enter USB Serial Number" - /> - handleUsbName(e.target.value)} - placeholder="Enter USB Name" - /> - handleUsbManufacturer(e.target.value)} - placeholder="Enter USB Manufacturer" - /> -
-
-
+ +