mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
chore/Deprecate browser mount (#752)
* chore/Deprecate browser mount No longer supported. * Remove device-side go code * Removed diskChannel and localFile * Removed RemoteVirtualMediaState.WebRTC Also removed dead go code (to make that lint happy!)
This commit is contained in:
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -49,7 +49,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-dom": "^19.1.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||
@@ -1970,9 +1970,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.8.tgz",
|
||||
"integrity": "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-dom": "^19.1.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useMemo, forwardRef, useEffect, useCallback } from "react";
|
||||
import { forwardRef, useEffect, useCallback } from "react";
|
||||
import {
|
||||
LuArrowUpFromLine,
|
||||
LuCheckCheck,
|
||||
LuLink,
|
||||
LuPlus,
|
||||
LuRadioReceiver,
|
||||
@@ -14,38 +11,17 @@ import { useLocation } from "react-router-dom";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { formatters } from "@/utils";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const { diskDataChannelStats } = useRTCStore();
|
||||
const { send } = useJsonRpc();
|
||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||
useMountMediaStore();
|
||||
|
||||
const bytesSentPerSecond = useMemo(() => {
|
||||
if (diskDataChannelStats.size < 2) return null;
|
||||
|
||||
const secondLastItem =
|
||||
Array.from(diskDataChannelStats)[diskDataChannelStats.size - 2];
|
||||
const lastItem = Array.from(diskDataChannelStats)[diskDataChannelStats.size - 1];
|
||||
|
||||
if (!secondLastItem || !lastItem) return 0;
|
||||
|
||||
const lastTime = lastItem[0];
|
||||
const secondLastTime = secondLastItem[0];
|
||||
const timeDelta = lastTime - secondLastTime;
|
||||
|
||||
const lastBytesSent = lastItem[1].bytesSent;
|
||||
const secondLastBytesSent = secondLastItem[1].bytesSent;
|
||||
const bytesDelta = lastBytesSent - secondLastBytesSent;
|
||||
|
||||
return bytesDelta / timeDelta;
|
||||
}, [diskDataChannelStats]);
|
||||
|
||||
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||
if ("error" in response) {
|
||||
@@ -94,42 +70,6 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const { source, filename, size, url, path } = remoteVirtualMediaState;
|
||||
|
||||
switch (source) {
|
||||
case "WebRTC":
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
<Card className="w-auto px-2 py-1">
|
||||
<div className="w-full truncate text-sm text-black dark:text-white">
|
||||
{formatters.truncateMiddle(filename, 50)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
<span>
|
||||
{bytesSentPerSecond !== null
|
||||
? `${formatters.bytes(bytesSentPerSecond)}/s`
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "HTTP":
|
||||
return (
|
||||
<div className="">
|
||||
@@ -202,18 +142,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
description="Mount an image to boot from or install an operating system."
|
||||
/>
|
||||
|
||||
{remoteVirtualMediaState?.source === "WebRTC" ? (
|
||||
<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 w-full items-center text-black">
|
||||
<div>Closing this tab will unmount the image</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
|
||||
@@ -105,9 +105,6 @@ export interface RTCState {
|
||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||
rpcDataChannel: RTCDataChannel | null;
|
||||
|
||||
diskChannel: RTCDataChannel | null;
|
||||
setDiskChannel: (channel: RTCDataChannel) => void;
|
||||
|
||||
peerConnectionState: RTCPeerConnectionState | null;
|
||||
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
||||
|
||||
@@ -160,9 +157,6 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||
peerConnectionState: null,
|
||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||
|
||||
diskChannel: null,
|
||||
setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }),
|
||||
|
||||
mediaStream: null,
|
||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
||||
|
||||
@@ -381,7 +375,7 @@ export const useSettingsStore = create(
|
||||
);
|
||||
|
||||
export interface RemoteVirtualMediaState {
|
||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
||||
source: "HTTP" | "Storage" | null;
|
||||
mode: "CDROM" | "Disk" | null;
|
||||
filename: string | null;
|
||||
url: string | null;
|
||||
@@ -390,13 +384,10 @@ export interface RemoteVirtualMediaState {
|
||||
}
|
||||
|
||||
export interface MountMediaState {
|
||||
localFile: File | null;
|
||||
setLocalFile: (file: MountMediaState["localFile"]) => void;
|
||||
|
||||
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
||||
|
||||
modalView: "mode" | "browser" | "url" | "device" | "upload" | "error" | null;
|
||||
modalView: "mode" | "url" | "device" | "upload" | "error" | null;
|
||||
setModalView: (view: MountMediaState["modalView"]) => void;
|
||||
|
||||
isMountMediaDialogOpen: boolean;
|
||||
@@ -410,9 +401,6 @@ export interface MountMediaState {
|
||||
}
|
||||
|
||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||
localFile: null,
|
||||
setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }),
|
||||
|
||||
remoteVirtualMediaState: null,
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
LuGlobe,
|
||||
LuLink,
|
||||
LuRadioReceiver,
|
||||
LuHardDrive,
|
||||
LuCheck,
|
||||
LuUpload,
|
||||
} from "react-icons/lu";
|
||||
@@ -50,7 +48,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
const {
|
||||
modalView,
|
||||
setModalView,
|
||||
setLocalFile,
|
||||
setRemoteVirtualMediaState,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
@@ -60,7 +57,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
const [incompleteFileName, setIncompleteFileName] = useState<string | null>(null);
|
||||
const [mountInProgress, setMountInProgress] = useState(false);
|
||||
function clearMountMediaState() {
|
||||
setLocalFile(null);
|
||||
setRemoteVirtualMediaState(null);
|
||||
}
|
||||
|
||||
@@ -131,35 +127,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
clearMountMediaState();
|
||||
}
|
||||
|
||||
function handleBrowserMount(file: File, mode: RemoteVirtualMediaState["mode"]) {
|
||||
console.log(`Mounting ${file.name} as ${mode}`);
|
||||
|
||||
setMountInProgress(true);
|
||||
send(
|
||||
"mountWithWebRTC",
|
||||
{ filename: file.name, size: file.size, mode },
|
||||
async resp => {
|
||||
if ("error" in resp) triggerError(resp.error.message);
|
||||
|
||||
clearMountMediaState();
|
||||
syncRemoteVirtualMediaState()
|
||||
.then(() => {
|
||||
// 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);
|
||||
navigate("..");
|
||||
})
|
||||
.catch(err => {
|
||||
triggerError(err instanceof Error ? err.message : String(err));
|
||||
})
|
||||
.finally(() => {
|
||||
setMountInProgress(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [selectedMode, setSelectedMode] = useState<"browser" | "url" | "device">("url");
|
||||
const [selectedMode, setSelectedMode] = useState<"url" | "device">("url");
|
||||
return (
|
||||
<AutoHeight>
|
||||
<div
|
||||
@@ -167,7 +135,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
"max-w-4xl": modalView === "mode",
|
||||
"max-w-2xl": modalView === "device",
|
||||
"max-w-xl":
|
||||
modalView === "browser" ||
|
||||
modalView === "url" ||
|
||||
modalView === "upload" ||
|
||||
modalView === "error",
|
||||
@@ -194,19 +161,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "browser" && (
|
||||
<BrowserFileView
|
||||
mountInProgress={mountInProgress}
|
||||
onMountFile={(file, mode) => {
|
||||
handleBrowserMount(file, mode);
|
||||
}}
|
||||
onBack={() => {
|
||||
setMountInProgress(false);
|
||||
setModalView("mode");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "url" && (
|
||||
<UrlView
|
||||
mountInProgress={mountInProgress}
|
||||
@@ -275,8 +229,8 @@ function ModeSelectionView({
|
||||
setSelectedMode,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
selectedMode: "browser" | "url" | "device";
|
||||
setSelectedMode: (mode: "browser" | "url" | "device") => void;
|
||||
selectedMode: "url" | "device";
|
||||
setSelectedMode: (mode: "url" | "device") => void;
|
||||
}) {
|
||||
const { setModalView } = useMountMediaStore();
|
||||
|
||||
@@ -292,14 +246,6 @@ function ModeSelectionView({
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
label: "Browser Mount",
|
||||
value: "browser",
|
||||
description: "Stream files directly from your browser",
|
||||
icon: LuGlobe,
|
||||
tag: "Coming Soon",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: "URL Mount",
|
||||
value: "url",
|
||||
@@ -338,7 +284,7 @@ function ModeSelectionView({
|
||||
<div
|
||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||
onClick={() =>
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||
disabled ? null : setSelectedMode(mode as "url" | "device")
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -394,119 +340,6 @@ function ModeSelectionView({
|
||||
);
|
||||
}
|
||||
|
||||
function BrowserFileView({
|
||||
onMountFile,
|
||||
onBack,
|
||||
mountInProgress,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
onMountFile: (file: File, mode: RemoteVirtualMediaState["mode"]) => void;
|
||||
mountInProgress: boolean;
|
||||
}) {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
setSelectedFile(file);
|
||||
|
||||
if (file?.name.endsWith(".iso")) {
|
||||
setUsbMode("CDROM");
|
||||
} else if (file?.name.endsWith(".img")) {
|
||||
setUsbMode("Disk");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMount = () => {
|
||||
if (selectedFile) {
|
||||
console.log(`Mounting ${selectedFile.name} as ${setUsbMode}`);
|
||||
onMountFile(selectedFile, usbMode);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from Browser"
|
||||
description="Select an image file to mount"
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
onClick={() => document.getElementById("file-upload")?.click()}
|
||||
className="block cursor-pointer select-none"
|
||||
>
|
||||
<div
|
||||
className="group animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
>
|
||||
<Card className="transition-all duration-300 outline-dashed">
|
||||
<div className="w-full px-4 py-12">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm leading-none font-semibold">
|
||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700">
|
||||
{formatters.bytes(selectedFile.size)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm leading-none font-semibold">
|
||||
Click to select a file
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700">
|
||||
Supported formats: ISO, IMG
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
accept=".iso, .img"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<Fieldset disabled={!selectedFile}>
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
onClick={handleMount}
|
||||
disabled={!selectedFile || mountInProgress}
|
||||
loading={mountInProgress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UrlView({
|
||||
onBack,
|
||||
onMount,
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
USBStates,
|
||||
useDeviceStore,
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
useNetworkStateStore,
|
||||
User,
|
||||
useRTCStore,
|
||||
@@ -132,7 +131,6 @@ export default function KvmIdRoute() {
|
||||
const {
|
||||
peerConnection, setPeerConnection,
|
||||
peerConnectionState, setPeerConnectionState,
|
||||
diskChannel, setDiskChannel,
|
||||
setMediaStream,
|
||||
setRpcDataChannel,
|
||||
isTurnServerInUse, setTurnServerInUse,
|
||||
@@ -484,18 +482,12 @@ export default function KvmIdRoute() {
|
||||
setRpcDataChannel(rpcDataChannel);
|
||||
};
|
||||
|
||||
const diskDataChannel = pc.createDataChannel("disk");
|
||||
diskDataChannel.onopen = () => {
|
||||
setDiskChannel(diskDataChannel);
|
||||
};
|
||||
|
||||
setPeerConnection(pc);
|
||||
}, [
|
||||
cleanupAndStopReconnecting,
|
||||
iceConfig?.iceServers,
|
||||
legacyHTTPSignaling,
|
||||
sendWebRTCSignal,
|
||||
setDiskChannel,
|
||||
setMediaStream,
|
||||
setPeerConnection,
|
||||
setPeerConnectionState,
|
||||
@@ -719,25 +711,6 @@ export default function KvmIdRoute() {
|
||||
}
|
||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||
|
||||
const { localFile } = useMountMediaStore();
|
||||
useEffect(() => {
|
||||
if (!diskChannel || !localFile) return;
|
||||
diskChannel.onmessage = async e => {
|
||||
console.debug("Received", e.data);
|
||||
const data = JSON.parse(e.data);
|
||||
const blob = localFile.slice(data.start, data.end);
|
||||
const buf = await blob.arrayBuffer();
|
||||
const header = new ArrayBuffer(16);
|
||||
const headerView = new DataView(header);
|
||||
headerView.setBigUint64(0, BigInt(data.start), false); // start offset, big-endian
|
||||
headerView.setBigUint64(8, BigInt(buf.byteLength), false); // length, big-endian
|
||||
const fullData = new Uint8Array(header.byteLength + buf.byteLength);
|
||||
fullData.set(new Uint8Array(header), 0);
|
||||
fullData.set(new Uint8Array(buf), header.byteLength);
|
||||
diskChannel.send(fullData);
|
||||
};
|
||||
}, [diskChannel, localFile]);
|
||||
|
||||
// System update
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user