From 6a2443c07c820f32bc721d2694fb0a1c4e313468 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 26 Feb 2025 00:53:26 +0100 Subject: [PATCH] feat(ui): Add dedicated mount route and refactor mount media dialog --- ui/src/components/popovers/MountPopover.tsx | 97 ++++++----- ui/src/main.tsx | 9 + .../devices.$id.mount.tsx} | 154 +++++++++--------- 3 files changed, 144 insertions(+), 116 deletions(-) rename ui/src/{components/MountMediaDialog.tsx => routes/devices.$id.mount.tsx} (90%) diff --git a/ui/src/components/popovers/MountPopover.tsx b/ui/src/components/popovers/MountPopover.tsx index 3446bd8..340afe2 100644 --- a/ui/src/components/popovers/MountPopover.tsx +++ b/ui/src/components/popovers/MountPopover.tsx @@ -15,19 +15,14 @@ import { } from "react-icons/lu"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "../../notifications"; -import MountMediaModal from "../MountMediaDialog"; import { useClose } from "@headlessui/react"; +import { useLocation, useNavigate } from "react-router-dom"; const MountPopopover = forwardRef((_props, ref) => { const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats); const [send] = useJsonRpc(); - const { - remoteVirtualMediaState, - isMountMediaDialogOpen, - setModalView, - setIsMountMediaDialogOpen, - setRemoteVirtualMediaState, - } = useMountMediaStore(); + const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } = + useMountMediaStore(); const bytesSentPerSecond = useMemo(() => { if (diskDataChannelStats.size < 2) return null; @@ -78,7 +73,7 @@ const MountPopopover = forwardRef((_props, ref) => {
- +
@@ -103,20 +98,25 @@ const MountPopopover = forwardRef((_props, ref) => {
-

Streaming from Browser

+

+ Streaming from Browser +

-
+
{formatters.truncateMiddle(filename, 50)}
-
+
{formatters.bytes(size ?? 0)}
- + {bytesSentPerSecond !== null ? `${formatters.bytes(bytesSentPerSecond)}/s` @@ -131,33 +131,49 @@ const MountPopopover = forwardRef((_props, ref) => { case "HTTP": return (
-
+
- +
-

Streaming from URL

-

{formatters.truncateMiddle(url, 55)}

-

{formatters.truncateMiddle(filename, 30)}

-

{formatters.bytes(size ?? 0)}

+

+ Streaming from URL +

+

+ {formatters.truncateMiddle(url, 55)} +

+

+ {formatters.truncateMiddle(filename, 30)} +

+

+ {formatters.bytes(size ?? 0)} +

); case "Storage": return (
-
+
- +
-

Mounted from JetKVM Storage

-

{formatters.truncateMiddle(path, 50)}

-

{formatters.truncateMiddle(filename, 30)}

-

{formatters.bytes(size ?? 0)}

+

+ Mounted from JetKVM Storage +

+

+ {formatters.truncateMiddle(path, 50)} +

+

+ {formatters.truncateMiddle(filename, 30)} +

+

+ {formatters.bytes(size ?? 0)} +

); default: @@ -165,14 +181,17 @@ const MountPopopover = forwardRef((_props, ref) => { } }; const close = useClose(); + const location = useLocation(); useEffect(() => { syncRemoteVirtualMediaState(); - }, [syncRemoteVirtualMediaState, isMountMediaDialogOpen]); + }, [syncRemoteVirtualMediaState, location.pathname]); + + const navigate = useNavigate(); return ( -
+
@@ -185,7 +204,7 @@ const MountPopopover = forwardRef((_props, ref) => {
-
+
Closing this tab will unmount the image
@@ -193,7 +212,7 @@ const MountPopopover = forwardRef((_props, ref) => { ) : null}
((_props, ref) => {
-
+
{renderGridCardContent()}
@@ -211,8 +230,8 @@ const MountPopopover = forwardRef((_props, ref) => {
{remoteVirtualMediaState ? ( -
-
+
+
Mounted as{" "} {remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"} @@ -244,7 +263,10 @@ const MountPopopover = forwardRef((_props, ref) => { d="M4.99933 0.775635L0 5.77546H10L4.99933 0.775635Z" fill="currentColor" /> - + @@ -261,16 +283,11 @@ const MountPopopover = forwardRef((_props, ref) => {
- -
{!remoteVirtualMediaState && (
((_props, ref) => { text="Add New Media" onClick={() => { setModalView("mode"); - setIsMountMediaDialogOpen(true); + navigate("mount"); }} LeadingIcon={LuPlus} /> diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 18872f1..b861f2a 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -31,6 +31,7 @@ import { CLOUD_API } from "./ui.config"; import OtherSessionRoute from "./routes/devices.$id.other-session"; import UpdateRoute from "./routes/devices.$id.update"; import LocalAuthRoute from "./routes/devices.$id.local-auth"; +import MountRoute from "./routes/devices.$id.mount"; export const isOnDevice = import.meta.env.MODE === "device"; export const isInCloud = !isOnDevice; @@ -91,6 +92,10 @@ if (isOnDevice) { path: "local-auth", element: , }, + { + path: "mount", + element: , + }, ], }, @@ -147,6 +152,10 @@ if (isOnDevice) { path: "local-auth", element: , }, + { + path: "mount", + element: , + }, ], }, { diff --git a/ui/src/components/MountMediaDialog.tsx b/ui/src/routes/devices.$id.mount.tsx similarity index 90% rename from ui/src/components/MountMediaDialog.tsx rename to ui/src/routes/devices.$id.mount.tsx index 892340b..46022a0 100644 --- a/ui/src/components/MountMediaDialog.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useRef, 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 { MountMediaState, RemoteVirtualMediaState, @@ -21,8 +20,8 @@ import { } from "react-icons/lu"; import { formatters } from "@/utils"; import { PlusCircleIcon } from "@heroicons/react/20/solid"; -import AutoHeight from "./AutoHeight"; -import { InputFieldWithLabel } from "./InputField"; +import AutoHeight from "@components/AutoHeight"; +import { InputFieldWithLabel } from "@/components/InputField"; import DebianIcon from "@/assets/debian-icon.png"; import UbuntuIcon from "@/assets/ubuntu-icon.png"; import FedoraIcon from "@/assets/fedora-icon.png"; @@ -33,34 +32,26 @@ import { TrashIcon } from "@heroicons/react/16/solid"; import { useJsonRpc } from "../hooks/useJsonRpc"; import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import notifications from "../notifications"; -import Fieldset from "./Fieldset"; +import Fieldset from "@/components/Fieldset"; import { isOnDevice } from "../main"; import { DEVICE_API } from "@/ui.config"; +import { useNavigate } from "react-router-dom"; -export default function MountMediaModal({ - open, - setOpen, -}: { - open: boolean; - setOpen: (open: boolean) => void; -}) { - return ( - setOpen(false)}> - - - ); +export default function MountRoute() { + const navigate = useNavigate(); + return navigate("..")} />; } -export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { +export function Dialog({ onClose }: { onClose: () => void }) { const { modalView, setModalView, setLocalFile, - setIsMountMediaDialogOpen, setRemoteVirtualMediaState, errorMessage, setErrorMessage, } = useMountMediaStore(); + const navigate = useNavigate(); const [incompleteFileName, setIncompleteFileName] = useState(null); const [mountInProgress, setMountInProgress] = useState(false); @@ -100,7 +91,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { clearMountMediaState(); syncRemoteVirtualMediaState() .then(() => { - setIsMountMediaDialogOpen(false); + navigate(".."); }) .catch(err => { triggerError(err instanceof Error ? err.message : String(err)); @@ -109,7 +100,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { setMountInProgress(false); }); - setIsMountMediaDialogOpen(false); + navigate(".."); }); } @@ -123,7 +114,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { clearMountMediaState(); syncRemoteVirtualMediaState() .then(() => { - setIsMountMediaDialogOpen(false); + false; }) .catch(err => { triggerError(err instanceof Error ? err.message : String(err)); @@ -156,7 +147,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { // We need to keep the local file in the store so that the browser can // continue to stream the file to the device setLocalFile(file); - setIsMountMediaDialogOpen(false); + navigate(".."); }) .catch(err => { triggerError(err instanceof Error ? err.message : String(err)); @@ -188,16 +179,16 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { JetKVM Logo JetKVM Logo {modalView === "mode" && ( setOpen(false)} + onClose={() => onClose()} selectedMode={selectedMode} setSelectedMode={setSelectedMode} /> @@ -261,7 +252,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { { - setOpen(false); + onClose(); setErrorMessage(null); }} onRetry={() => { @@ -291,7 +282,7 @@ function ModeSelectionView({ return (
-
+

Virtual Media Source

@@ -345,7 +336,7 @@ function ModeSelectionView({ )} >
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device") } @@ -353,7 +344,7 @@ function ModeSelectionView({
- +
@@ -373,7 +364,7 @@ function ModeSelectionView({ value={mode} disabled={disabled} checked={selectedMode === mode} - className="absolute w-4 h-4 text-blue-700 right-4 top-4" + className="absolute right-4 top-4 h-4 w-4 text-blue-700" />
@@ -381,13 +372,13 @@ function ModeSelectionView({ ))}
-
+
-
+
e.stopPropagation()} // Prevent double-firing of onSelect /> ) : ( @@ -1549,7 +1551,7 @@ function UsbModeSelector({ setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void; }) { return ( -
+