mirror of https://github.com/jetkvm/kvm.git
feat(ui): Add dedicated mount route and refactor mount media dialog
This commit is contained in:
parent
c4f85b706f
commit
6a2443c07c
|
@ -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<HTMLDivElement, object>((_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<HTMLDivElement, object>((_props, ref) => {
|
|||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<PlusCircleIcon className="w-4 h-4 text-blue-700 shrink-0 dark:text-white" />
|
||||
<PlusCircleIcon className="h-4 w-4 shrink-0 text-blue-700 dark:text-white" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -103,20 +98,25 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<LuCheckCheck className="h-5 text-green-500" />
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">Streaming from Browser</h3>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||
Streaming from Browser
|
||||
</h3>
|
||||
</div>
|
||||
<Card className="w-auto px-2 py-1">
|
||||
<div className="w-full text-sm text-black truncate dark:text-white">
|
||||
<div className="w-full truncate text-sm text-black dark:text-white">
|
||||
{formatters.truncateMiddle(filename, 50)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col items-center my-2 gap-y-2">
|
||||
<div className="my-2 flex flex-col items-center gap-y-2">
|
||||
<div className="w-full text-sm text-slate-900 dark:text-slate-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{formatters.bytes(size ?? 0)}</span>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<LuArrowUpFromLine className="h-4 text-blue-700 dark:text-blue-500" strokeWidth={2} />
|
||||
<LuArrowUpFromLine
|
||||
className="h-4 text-blue-700 dark:text-blue-500"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span>
|
||||
{bytesSentPerSecond !== null
|
||||
? `${formatters.bytes(bytesSentPerSecond)}/s`
|
||||
|
@ -131,33 +131,49 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
case "HTTP":
|
||||
return (
|
||||
<div className="">
|
||||
<div className="inline-block mb-0">
|
||||
<div className="mb-0 inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuLink className="w-4 h-4 text-blue-700 dark:text-blue-500 shrink-0" />
|
||||
<LuLink className="h-4 w-4 shrink-0 text-blue-700 dark:text-blue-500" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">Streaming from URL</h3>
|
||||
<p className="text-sm truncate text-slate-900 dark:text-slate-100">{formatters.truncateMiddle(url, 55)}</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">{formatters.truncateMiddle(filename, 30)}</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">{formatters.bytes(size ?? 0)}</p>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||
Streaming from URL
|
||||
</h3>
|
||||
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(url, 55)}
|
||||
</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(filename, 30)}
|
||||
</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.bytes(size ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
case "Storage":
|
||||
return (
|
||||
<div className="">
|
||||
<div className="inline-block mb-0">
|
||||
<div className="mb-0 inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuRadioReceiver className="w-4 h-4 text-blue-700 dark:text-blue-500 shrink-0" />
|
||||
<LuRadioReceiver className="h-4 w-4 shrink-0 text-blue-700 dark:text-blue-500" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">Mounted from JetKVM Storage</h3>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">{formatters.truncateMiddle(path, 50)}</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">{formatters.truncateMiddle(filename, 30)}</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">{formatters.bytes(size ?? 0)}</p>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||
Mounted from JetKVM Storage
|
||||
</h3>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(path, 50)}
|
||||
</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(filename, 30)}
|
||||
</p>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.bytes(size ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
|
@ -165,14 +181,17 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
}
|
||||
};
|
||||
const close = useClose();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
syncRemoteVirtualMediaState();
|
||||
}, [syncRemoteVirtualMediaState, isMountMediaDialogOpen]);
|
||||
}, [syncRemoteVirtualMediaState, location.pathname]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<GridCard>
|
||||
<div className="p-4 py-3 space-y-4">
|
||||
<div className="space-y-4 p-4 py-3">
|
||||
<div ref={ref} className="grid h-full grid-rows-headerBody">
|
||||
<div className="h-full space-y-4 ">
|
||||
<div className="space-y-4">
|
||||
|
@ -185,7 +204,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<Card>
|
||||
<div className="flex items-center gap-x-1.5 px-2.5 py-2 text-sm">
|
||||
<ExclamationTriangleIcon className="h-4 text-yellow-500" />
|
||||
<div className="flex items-center w-full text-black">
|
||||
<div className="flex w-full items-center text-black">
|
||||
<div>Closing this tab will unmount the image</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -193,7 +212,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
) : null}
|
||||
|
||||
<div
|
||||
className="space-y-2 opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -203,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<div className="group">
|
||||
<Card>
|
||||
<div className="w-full px-4 py-8">
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{renderGridCardContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -211,8 +230,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</div>
|
||||
</div>
|
||||
{remoteVirtualMediaState ? (
|
||||
<div className="flex items-center justify-between text-xs select-none">
|
||||
<div className="text-white select-none dark:text-slate-300">
|
||||
<div className="flex select-none items-center justify-between text-xs">
|
||||
<div className="select-none text-white dark:text-slate-300">
|
||||
<span>Mounted as</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
||||
|
@ -244,7 +263,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
d="M4.99933 0.775635L0 5.77546H10L4.99933 0.775635Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10 7.49976H0V9.22453H10V7.49976Z" fill="currentColor" />
|
||||
<path
|
||||
d="M10 7.49976H0V9.22453H10V7.49976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3137_1186">
|
||||
|
@ -261,16 +283,11 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MountMediaModal
|
||||
open={isMountMediaDialogOpen}
|
||||
setOpen={setIsMountMediaDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!remoteVirtualMediaState && (
|
||||
<div
|
||||
className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -290,7 +307,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
text="Add New Media"
|
||||
onClick={() => {
|
||||
setModalView("mode");
|
||||
setIsMountMediaDialogOpen(true);
|
||||
navigate("mount");
|
||||
}}
|
||||
LeadingIcon={LuPlus}
|
||||
/>
|
||||
|
|
|
@ -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: <LocalAuthRoute />,
|
||||
},
|
||||
{
|
||||
path: "mount",
|
||||
element: <MountRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -147,6 +152,10 @@ if (isOnDevice) {
|
|||
path: "local-auth",
|
||||
element: <LocalAuthRoute />,
|
||||
},
|
||||
{
|
||||
path: "mount",
|
||||
element: <MountRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 (
|
||||
<Modal open={open} onClose={() => setOpen(false)}>
|
||||
<Dialog setOpen={setOpen} />
|
||||
</Modal>
|
||||
);
|
||||
export default function MountRoute() {
|
||||
const navigate = useNavigate();
|
||||
return <Dialog onClose={() => 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<string | null>(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 }) {
|
|||
<img
|
||||
src={LogoBlueIcon}
|
||||
alt="JetKVM Logo"
|
||||
className="h-[24px] dark:hidden block"
|
||||
className="block h-[24px] dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt="JetKVM Logo"
|
||||
className="h-[24px] dark:block hidden dark:!mt-0"
|
||||
className="hidden h-[24px] dark:!mt-0 dark:block"
|
||||
/>
|
||||
{modalView === "mode" && (
|
||||
<ModeSelectionView
|
||||
onClose={() => setOpen(false)}
|
||||
onClose={() => onClose()}
|
||||
selectedMode={selectedMode}
|
||||
setSelectedMode={setSelectedMode}
|
||||
/>
|
||||
|
@ -261,7 +252,7 @@ export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) {
|
|||
<ErrorView
|
||||
errorMessage={errorMessage}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
onClose();
|
||||
setErrorMessage(null);
|
||||
}}
|
||||
onRetry={() => {
|
||||
|
@ -291,7 +282,7 @@ function ModeSelectionView({
|
|||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="space-y-0 asnimate-fadeIn">
|
||||
<div className="asnimate-fadeIn space-y-0">
|
||||
<h2 className="text-lg font-bold leading-tight dark:text-white">
|
||||
Virtual Media Source
|
||||
</h2>
|
||||
|
@ -345,7 +336,7 @@ function ModeSelectionView({
|
|||
)}
|
||||
>
|
||||
<div
|
||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||
className="relative z-50 flex select-none flex-col items-start p-4"
|
||||
onClick={() =>
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||
}
|
||||
|
@ -353,7 +344,7 @@ function ModeSelectionView({
|
|||
<div>
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<Icon className="w-4 h-4 text-blue-700 shrink-0 dark:text-blue-400" />
|
||||
<Icon className="h-4 w-4 shrink-0 text-blue-700 dark:text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -381,13 +372,13 @@ function ModeSelectionView({
|
|||
))}
|
||||
</div>
|
||||
<div
|
||||
className="flex justify-end opacity-0 animate-fadeIn"
|
||||
className="flex animate-fadeIn justify-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
}}
|
||||
>
|
||||
<div className="flex pt-2 gap-x-2">
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Button size="MD" theme="blank" onClick={onClose} text="Cancel" />
|
||||
<Button
|
||||
size="MD"
|
||||
|
@ -445,18 +436,18 @@ function BrowserFileView({
|
|||
className="block cursor-pointer select-none"
|
||||
>
|
||||
<div
|
||||
className="opacity-0 group animate-fadeIn"
|
||||
className="group animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
>
|
||||
<Card className="transition-all duration-300 outline-dashed hover:bg-blue-50/50">
|
||||
<Card className="outline-dashed transition-all duration-300 hover:bg-blue-50/50">
|
||||
<div className="w-full px-4 py-12">
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<LuHardDrive className="w-6 h-6 mx-auto text-blue-700" />
|
||||
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm font-semibold leading-none">
|
||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
||||
</h3>
|
||||
|
@ -467,7 +458,7 @@ function BrowserFileView({
|
|||
</>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="w-6 h-6 mx-auto text-blue-700" />
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm font-semibold leading-none">
|
||||
Click to select a file
|
||||
</h3>
|
||||
|
@ -491,7 +482,7 @@ function BrowserFileView({
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-end justify-between w-full opacity-0 animate-fadeIn"
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -586,7 +577,7 @@ function UrlView({
|
|||
/>
|
||||
|
||||
<div
|
||||
className="opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -601,7 +592,7 @@ function UrlView({
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-end justify-between w-full opacity-0 animate-fadeIn"
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -627,7 +618,7 @@ function UrlView({
|
|||
|
||||
<hr className="border-slate-800/30 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -636,7 +627,7 @@ function UrlView({
|
|||
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
||||
Popular images
|
||||
</h2>
|
||||
<Card className="w-full divide-y divide-y-slate-800/30 dark:divide-slate-300/20">
|
||||
<Card className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
||||
{popularImages.map((image, index) => (
|
||||
<div key={index} className="flex items-center justify-between gap-x-4 p-3.5">
|
||||
<div className="flex items-center gap-x-4">
|
||||
|
@ -805,7 +796,7 @@ function DeviceFileView({
|
|||
description="Select an image to mount from the JetKVM storage"
|
||||
/>
|
||||
<div
|
||||
className="w-full opacity-0 animate-fadeIn"
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -816,7 +807,7 @@ function DeviceFileView({
|
|||
<div className="flex items-center justify-center py-8 text-center">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="w-6 h-6 mx-auto text-blue-700 dark:text-blue-500" />
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
No images available
|
||||
</h3>
|
||||
|
@ -835,7 +826,7 @@ function DeviceFileView({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full divide-y divide-y-slate-800/30 dark:divide-slate-300/20">
|
||||
<div className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
||||
{currentFiles.map((file, index) => (
|
||||
<PreUploadedImageItem
|
||||
key={index}
|
||||
|
@ -888,7 +879,7 @@ function DeviceFileView({
|
|||
|
||||
{onStorageFiles.length > 0 ? (
|
||||
<div
|
||||
className="flex items-end justify-between opacity-0 animate-fadeIn"
|
||||
className="flex animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -916,7 +907,7 @@ function DeviceFileView({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex items-end justify-end opacity-0 animate-fadeIn"
|
||||
className="flex animate-fadeIn items-end justify-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -929,31 +920,39 @@ function DeviceFileView({
|
|||
)}
|
||||
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="space-y-2 opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.20s",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">Available Storage</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">{percentageUsed}% used</span>
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
|
||||
<div
|
||||
className="h-full transition-all duration-300 ease-in-out bg-blue-700 rounded-sm dark:bg-blue-500"
|
||||
className="h-full rounded-sm bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
|
||||
style={{ width: `${percentageUsed}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">{formatters.bytes(bytesUsed)} used</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">{formatters.bytes(bytesFree)} free</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{onStorageFiles.length > 0 && (
|
||||
<div
|
||||
className="w-full opacity-0 animate-fadeIn"
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.25s",
|
||||
|
@ -1245,7 +1244,7 @@ function UploadFileView({
|
|||
}
|
||||
/>
|
||||
<div
|
||||
className="space-y-2 opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -1261,17 +1260,18 @@ function UploadFileView({
|
|||
<div className="group">
|
||||
<Card
|
||||
className={cx("transition-all duration-300", {
|
||||
"cursor-pointer hover:bg-blue-900/50 dark:hover:bg-blue-900/50": uploadState === "idle",
|
||||
"cursor-pointer hover:bg-blue-900/50 dark:hover:bg-blue-900/50":
|
||||
uploadState === "idle",
|
||||
})}
|
||||
>
|
||||
<div className="h-[186px] w-full px-4">
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{uploadState === "idle" && (
|
||||
<div className="space-y-1">
|
||||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<PlusCircleIcon className="w-4 h-4 text-blue-500 dark:text-blue-400 shrink-0" />
|
||||
<PlusCircleIcon className="h-4 w-4 shrink-0 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1291,11 +1291,11 @@ function UploadFileView({
|
|||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuUpload className="w-4 h-4 text-blue-500 dark:text-blue-400 shrink-0" />
|
||||
<LuUpload className="h-4 w-4 shrink-0 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-black leading-non dark:text-white">
|
||||
<h3 className="leading-non text-lg font-semibold text-black dark:text-white">
|
||||
Uploading {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
|
@ -1304,7 +1304,7 @@ function UploadFileView({
|
|||
<div className="w-full space-y-2">
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-700">
|
||||
<div
|
||||
className="h-3.5 rounded-full bg-blue-700 dark:bg-blue-500 transition-all duration-500 ease-linear"
|
||||
className="h-3.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
@ -1325,7 +1325,7 @@ function UploadFileView({
|
|||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuCheck className="w-4 h-4 text-blue-500 dark:text-blue-400 shrink-0" />
|
||||
<LuCheck className="h-4 w-4 shrink-0 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1350,13 +1350,15 @@ function UploadFileView({
|
|||
className="hidden"
|
||||
accept=".iso, .img"
|
||||
/>
|
||||
{fileError && <p className="mt-2 text-sm text-red-600 dark:text-red-400">{fileError}</p>}
|
||||
{fileError && (
|
||||
<p className="mt-2 text-sm text-red-600 dark:text-red-400">{fileError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Display upload error if present */}
|
||||
{uploadError && (
|
||||
<div
|
||||
className="mt-2 text-sm text-red-600 truncate opacity-0 dark:text-red-400 animate-fadeIn"
|
||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 opacity-0 dark:text-red-400"
|
||||
style={{ animationDuration: "0.7s" }}
|
||||
>
|
||||
Error: {uploadError}
|
||||
|
@ -1364,13 +1366,13 @@ function UploadFileView({
|
|||
)}
|
||||
|
||||
<div
|
||||
className="flex items-end w-full opacity-0 animate-fadeIn"
|
||||
className="flex w-full animate-fadeIn items-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-end w-full space-x-2">
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
{uploadState === "uploading" ? (
|
||||
<Button
|
||||
size="MD"
|
||||
|
@ -1412,7 +1414,7 @@ function ErrorView({
|
|||
<div className="w-full space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<ExclamationTriangleIcon className="w-6 h-6" />
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<h2 className="text-lg font-bold leading-tight">Mount Error</h2>
|
||||
</div>
|
||||
<p className="text-sm leading-snug text-slate-600">
|
||||
|
@ -1420,7 +1422,7 @@ function ErrorView({
|
|||
</p>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<Card className="p-4 border border-red-200 bg-red-50">
|
||||
<Card className="border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-sm font-medium text-red-800">{errorMessage}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
@ -1480,12 +1482,12 @@ function PreUploadedImageItem({
|
|||
<div className="flex items-center gap-x-1 text-slate-600 dark:text-slate-400">
|
||||
{formatters.date(new Date(uploadedAt), { month: "short" })}
|
||||
</div>
|
||||
<div className="mx-1 h-[10px] w-[1px] bg-slate-300 dark:bg-slate-600 text-slate-300"></div>
|
||||
<div className="mx-1 h-[10px] w-[1px] bg-slate-300 text-slate-300 dark:bg-slate-600"></div>
|
||||
<div className="text-gray-600 dark:text-slate-400">{size}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex items-center select-none gap-x-3">
|
||||
<div className="relative flex select-none items-center gap-x-3">
|
||||
<div
|
||||
className={cx("opacity-0 transition-opacity duration-200", {
|
||||
"w-auto opacity-100": isHovering,
|
||||
|
@ -1509,7 +1511,7 @@ function PreUploadedImageItem({
|
|||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
name={name}
|
||||
className="w-3 h-3 text-blue-700 bg-white dark:bg-slate-800 border-slate-800/30 dark:border-slate-300/20 focus:ring-blue-500 disabled:opacity-30"
|
||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 focus:ring-blue-500 disabled:opacity-30 dark:border-slate-300/20 dark:bg-slate-800"
|
||||
onClick={e => e.stopPropagation()} // Prevent double-firing of onSelect
|
||||
/>
|
||||
) : (
|
||||
|
@ -1549,7 +1551,7 @@ function UsbModeSelector({
|
|||
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-start space-y-1 select-none">
|
||||
<div className="flex select-none flex-col items-start space-y-1">
|
||||
<label className="text-sm font-semibold text-black dark:text-white">Mount as</label>
|
||||
<div className="flex space-x-4">
|
||||
<label htmlFor="cdrom" className="flex items-center">
|
||||
|
@ -1559,7 +1561,7 @@ function UsbModeSelector({
|
|||
name="mountType"
|
||||
onChange={() => setUsbMode("CDROM")}
|
||||
checked={usbMode === "CDROM"}
|
||||
className="w-3 h-3 text-blue-700 transition-opacity bg-white border-slate-800/30 focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
CD/DVD
|
||||
|
@ -1573,10 +1575,10 @@ function UsbModeSelector({
|
|||
disabled
|
||||
checked={usbMode === "Disk"}
|
||||
onChange={() => setUsbMode("Disk")}
|
||||
className="w-3 h-3 text-blue-700 transition-opacity bg-white border-slate-800/30 focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<div className="flex flex-col ml-2 gap-y-0">
|
||||
<span className="text-sm font-medium leading-none opacity-50 text-slate-900 dark:text-white">
|
||||
<div className="ml-2 flex flex-col gap-y-0">
|
||||
<span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
|
||||
Disk
|
||||
</span>
|
||||
<div className="text-[10px] text-slate-500 dark:text-slate-400">
|
Loading…
Reference in New Issue