Files
VirtWeb/virtweb_frontend/src/routes/VNCRoute.tsx
Pierre HUBERT f5202f596d
All checks were successful
continuous-integration/drone/push Build is passing
Fix all ESLint errors
2025-03-28 12:25:04 +01:00

156 lines
4.0 KiB
TypeScript

import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import FullscreenIcon from "@mui/icons-material/Fullscreen";
import FullscreenExitIcon from "@mui/icons-material/FullscreenExit";
import KeyboardAltIcon from "@mui/icons-material/KeyboardAlt";
import { IconButton, Tooltip } from "@mui/material";
import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { VncScreen, VncScreenHandle } from "react-vnc";
import { ServerApi } from "../api/ServerApi";
import { VMApi, VMInfo } from "../api/VMApi";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { time } from "../utils/DateUtils";
import { AsyncWidget } from "../widgets/AsyncWidget";
interface VNCTokenInfo {
url: string;
expire: number;
}
export function VNCRoute(): React.ReactElement {
const { uuid } = useParams();
const [vm, setVM] = React.useState<VMInfo | undefined>();
const load = async () => {
setVM(await VMApi.GetSingle(uuid!));
};
return (
<AsyncWidget
loadKey={uuid}
load={load}
errMsg="Failed to load VM information!"
build={() => <VNCInner vm={vm!} />}
/>
);
}
function VNCInner(p: { vm: VMInfo }): React.ReactElement {
const snackbar = useSnackbar();
const navigate = useNavigate();
const [token, setToken] = React.useState<VNCTokenInfo | undefined>();
const [counter, setCounter] = React.useState(1);
const [connected, setConnected] = React.useState(false);
const vncRef = React.useRef<HTMLDivElement>(null);
const vncScreenRef = React.useRef<VncScreenHandle>(null);
const connect = async (force: boolean) => {
try {
if (force) setCounter(counter + 1);
// Check if getting new time is useless
if ((token?.expire ?? 0) > time()) return;
setToken(undefined);
const u = await VMApi.OneShotVNCURL(p.vm);
console.info("VNC URL", u);
if (!token)
setToken({
expire: time() + ServerApi.Config.constraints.vnc_token_duration,
url: u,
});
} catch (e) {
console.error(e);
snackbar("Failed to initialize VNC connection!");
}
};
const disconnected = () => {
setConnected(false);
connect(true);
};
const goBack = () => {
navigate(p.vm.ViewURL);
};
const goFullScreen = () => {
if (vncRef.current) vncRef.current.requestFullscreen();
};
const exitFullScreen = () => {
document.exitFullscreen();
};
useEffect(() => {
connect(false);
if (vncRef.current) {
vncRef.current.onfullscreenchange = () => {
setCounter(counter + 1);
};
}
});
if (token === undefined)
return <p>Please wait, connecting to the machine...</p>;
return (
<div ref={vncRef} style={{ display: "flex" }}>
{/* Controls */}
<div style={{ display: "flex", flexDirection: "column" }}>
<IconButton onClick={goBack}>
<ArrowBackIcon />
</IconButton>
{/* Toggle fullscreen */}
{!document.fullscreenElement ? (
<IconButton onClick={goFullScreen}>
<FullscreenIcon />
</IconButton>
) : (
<IconButton onClick={exitFullScreen}>
<FullscreenExitIcon />
</IconButton>
)}
{/* Keystrokes */}
{connected && (
<Tooltip title="Send Ctrl+Alt+Del">
<IconButton onClick={() => vncScreenRef.current?.sendCtrlAltDel()}>
<KeyboardAltIcon />
</IconButton>
</Tooltip>
)}
</div>
{/* Screen */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
}}
>
<VncScreen
ref={vncScreenRef}
url={token.url}
onDisconnect={() => {
console.info("VNC disconnected " + token.url);
disconnected();
}}
onConnect={() => {
setConnected(true);
}}
/>
</div>
</div>
);
}