feat: add pve vnc session

This commit is contained in:
2024-11-06 17:32:39 +00:00
parent bde42ca729
commit 2adde048b0
11 changed files with 379 additions and 89 deletions
@@ -1,32 +1,45 @@
import React from "react";
import Terminal from "./terminal";
import { BASE_WS_URL } from "@/lib/api";
import VNCViewer from "./vncviewer";
type SSHSessionProps = {
type: "ssh";
options: {
params: {
serverId: string;
};
};
type Props = SSHSessionProps;
type PVESessionProps = {
type: "pve";
params: {
client: "vnc" | "xtermjs";
serverId: string;
};
};
export type InteractiveSessionProps = SSHSessionProps | PVESessionProps;
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
const query = new URLSearchParams({
...params,
});
const InteractiveSession = ({ type, options }: Props) => {
switch (type) {
case "ssh":
const params = new URLSearchParams({
serverId: options.serverId,
token: "token",
});
return (
<Terminal client="xtermjs" wsUrl={`${BASE_WS_URL}/ws/ssh?${params}`} />
return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />;
case "pve":
const url = `${BASE_WS_URL}/ws/pve?${query}`;
return params.client === "vnc" ? (
<VNCViewer url={url} />
) : (
<Terminal wsUrl={url} />
);
default:
throw new Error("Unknown interactive session type");
}
return null;
};
export default InteractiveSession;
+2 -2
View File
@@ -27,13 +27,13 @@ const Keys = {
};
type XTermJsProps = {
client: "xtermjs";
client?: "xtermjs";
wsUrl: string;
};
type TerminalProps = ComponentPropsWithoutRef<typeof View> & XTermJsProps;
const Terminal = ({ client, style, ...props }: TerminalProps) => {
const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
const xtermRef = React.useRef<XTermRef>(null);
const send = (data: string) => {
@@ -0,0 +1,90 @@
"use dom";
import React, { useEffect, useRef } from "react";
type VNCViewerProps = {
url: string;
};
const VNCViewer = ({ ...props }: VNCViewerProps) => {
const screenRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let clean = () => {};
(async function () {
// Dynamically load noVNC library
const { default: RFB } = await import("@novnc/novnc/lib/rfb");
const rfb = new RFB(screenRef.current!, props.url);
rfb.scaleViewport = true;
// @ts-ignore
const ws: WebSocket = rfb._sock._websocket;
let password: string | null = null;
const onConnect = () => {
// console.log("Connected");
};
const onDisconnect = () => {
// console.log("Disconnected");
};
const onMessage = (e: MessageEvent) => {
const msg = String(e.data);
// Capture password from server
if (msg.startsWith("\x01")) {
password = msg.substring(1);
ws.removeEventListener("message", onMessage);
}
};
const onCredentialsRequired = () => {
rfb.sendCredentials({
password: password || "",
username: "",
target: "",
});
password = null;
};
// const onDesktopName = (e: CustomEvent<{ name: string }>) => {
// console.log("Desktop name:", e.detail.name);
// };
ws.addEventListener("message", onMessage);
rfb.addEventListener("connect", onConnect);
rfb.addEventListener("disconnect", onDisconnect);
rfb.addEventListener("credentialsrequired", onCredentialsRequired);
// rfb.addEventListener("desktopname", onDesktopName);
// Hack: trigger scale update on visibility change
const observer = new ResizeObserver(([entry]) => {
if (entry.contentRect.width > 0 && rfb.scaleViewport) {
(rfb as any)._updateScale();
}
});
observer.observe(screenRef.current!);
clean = () => {
ws.removeEventListener("message", onMessage);
rfb.disconnect();
rfb.removeEventListener("connect", onConnect);
rfb.removeEventListener("disconnect", onDisconnect);
rfb.removeEventListener("credentialsrequired", onCredentialsRequired);
// rfb.removeEventListener("desktopname", onDesktopName);
observer.disconnect();
};
})();
return () => {
clean();
};
}, []);
return <div ref={screenRef} style={{ width: "100%", height: "100vh" }}></div>;
};
export default VNCViewer;
+13 -4
View File
@@ -1,10 +1,11 @@
"use dom";
import React, { CSSProperties, FC, forwardRef, useEffect, useRef } from "react";
import React, { CSSProperties, forwardRef, useEffect, useRef } from "react";
import {
DOMImperativeFactory,
DOMProps,
useDOMImperativeHandle,
IS_DOM,
} from "expo/dom";
import { Terminal as XTerm } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
@@ -19,9 +20,6 @@ type XTermJsProps = {
wsUrl: string;
};
// @ts-ignore
const IS_DOM = typeof ReactNativeWebView !== "undefined";
export interface XTermRef extends DOMImperativeFactory {
send: (...args: JSONValue[]) => void;
}
@@ -89,6 +87,15 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
ws.addEventListener("close", onClose);
xterm.onResize(resizeTerminal);
window.addEventListener("resize", onResize);
// Hack: trigger scale update on visibility change
const observer = new ResizeObserver(([entry]) => {
if (entry.contentRect.width > 0) {
fitAddon.fit();
}
});
observer.observe(containerRef.current!);
onLoad?.();
return () => {
@@ -100,6 +107,8 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
ws.removeEventListener("open", onOpen);
ws.removeEventListener("close", onClose);
window.removeEventListener("resize", onResize);
observer.disconnect();
};
}, [wsUrl]);