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)} + /> + + onSetUsbConfig(usbConfig)} + /> + + + {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" - /> - - { - if (Object.values(usbConfig).every(function(i) { return Boolean(i); })) { - handleUsbConfigChange(usbConfig); - notifications.success("Successfully updated USB Config") - } else { - notifications.error("Failed to update USB config"); - } - }} - /> - - + + { + setUsbConfigModalView("updateUsbConfig") + setIsUsbConfigDialogOpen(true); + }} + /> + )} + { + // Revalidate the current route to refresh the local device status and dependent UI components + revalidator.revalidate(); + setIsUsbConfigDialogOpen(x); + }} + /> ); } \ No newline at end of file diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index b4cfbec..a6b4451 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -528,3 +528,19 @@ export const useLocalAuthModalStore = create(set => ({ setModalView: view => set({ modalView: view }), setErrorMessage: message => set({ errorMessage: message }), })); + +interface UsbConfigModalState { + modalView: + | "updateUsbConfig" + | "updateUsbConfigSuccess"; + errorMessage: string | null; + setModalView: (view: UsbConfigModalState["modalView"]) => void; + setErrorMessage: (message: string | null) => void; +} + +export const useUsbConfigModalStore = create(set => ({ + modalView: "updateUsbConfig", + errorMessage: null, + setModalView: view => set({ modalView: view }), + setErrorMessage: message => set({ errorMessage: message }), +})); \ No newline at end of file
+ Set custom USB parameters to control the device USB emulation. The device will rebind once the parameters are updated. +
{error}
{description}