mirror of https://github.com/jetkvm/kvm.git
feat(ui): Add other session handling route and modal
This commit is contained in:
parent
ba0c937e2a
commit
a2652c5265
|
@ -17,11 +17,11 @@ export default function Modal({
|
||||||
<Dialog open={open} onClose={onClose} className="relative z-10">
|
<Dialog open={open} onClose={onClose} className="relative z-10">
|
||||||
<DialogBackdrop
|
<DialogBackdrop
|
||||||
transition
|
transition
|
||||||
className="fixed inset-0 bg-gray-500/75 dark:bg-slate-900/90 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
|
@ -30,9 +30,12 @@ export default function Modal({
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="inline-block w-full text-left pointer-events-auto">
|
<div className="pointer-events-auto inline-block w-full text-left">
|
||||||
<div className="flex justify-center" onClick={onClose}>
|
<div className="flex justify-center" onClick={onClose}>
|
||||||
<div className="w-full pointer-events-none" onClick={e => e.stopPropagation()}>
|
<div
|
||||||
|
className="pointer-events-none w-full"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
|
||||||
import WelcomeRoute from "./routes/welcome-local";
|
import WelcomeRoute from "./routes/welcome-local";
|
||||||
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||||
import { CLOUD_API } from "./ui.config";
|
import { CLOUD_API } from "./ui.config";
|
||||||
|
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||||
|
|
||||||
export const isOnDevice = import.meta.env.MODE === "device";
|
export const isOnDevice = import.meta.env.MODE === "device";
|
||||||
export const isInCloud = !isOnDevice;
|
export const isInCloud = !isOnDevice;
|
||||||
|
@ -75,7 +76,14 @@ if (isOnDevice) {
|
||||||
errorElement: <ErrorBoundary />,
|
errorElement: <ErrorBoundary />,
|
||||||
element: <DeviceRoute />,
|
element: <DeviceRoute />,
|
||||||
loader: DeviceRoute.loader,
|
loader: DeviceRoute.loader,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "other-session",
|
||||||
|
element: <OtherSessionRoute />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/adopt",
|
path: "/adopt",
|
||||||
element: <AdoptRoute />,
|
element: <AdoptRoute />,
|
||||||
|
@ -116,6 +124,12 @@ if (isOnDevice) {
|
||||||
path: "devices/:id",
|
path: "devices/:id",
|
||||||
element: <DeviceRoute />,
|
element: <DeviceRoute />,
|
||||||
loader: DeviceRoute.loader,
|
loader: DeviceRoute.loader,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "other-session",
|
||||||
|
element: <OtherSessionRoute />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "devices/:id/deregister",
|
path: "devices/:id/deregister",
|
||||||
|
@ -164,8 +178,8 @@ function ErrorBoundary() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="h-full w-full">
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
IconElm={ExclamationTriangleIcon}
|
IconElm={ExclamationTriangleIcon}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import LogoBlue from "@/assets/logo-blue.svg";
|
||||||
|
import LogoWhite from "@/assets/logo-white.svg";
|
||||||
|
|
||||||
|
interface ContextType {
|
||||||
|
connectWebRTC: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OtherSessionRoute() {
|
||||||
|
const outletContext = useOutletContext<ContextType>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Function to handle closing the modal
|
||||||
|
const handleClose = () => {
|
||||||
|
outletContext?.connectWebRTC().then(() => navigate(".."));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="relative mx-auto max-w-md text-left pointer-events-auto">
|
||||||
|
<div className="p-10">
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div className="h-[24px]">
|
||||||
|
<img src={LogoBlue} alt="" className="h-full dark:hidden" />
|
||||||
|
<img src={LogoWhite} alt="" className="hidden h-full dark:block" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold dark:text-white">
|
||||||
|
Another Active Session Detected
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Only one active session is supported at a time. Would you like to take over
|
||||||
|
this session?
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-start space-x-4">
|
||||||
|
<Button size="SM" theme="primary" text="Use Here" onClick={handleClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -16,10 +16,12 @@ import {
|
||||||
import WebRTCVideo from "@components/WebRTCVideo";
|
import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
import {
|
import {
|
||||||
LoaderFunctionArgs,
|
LoaderFunctionArgs,
|
||||||
|
Outlet,
|
||||||
Params,
|
Params,
|
||||||
redirect,
|
redirect,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
|
useOutlet,
|
||||||
useParams,
|
useParams,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
@ -34,9 +36,9 @@ import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import FocusTrap from "focus-trap-react";
|
import FocusTrap from "focus-trap-react";
|
||||||
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
|
|
||||||
import Terminal from "@components/Terminal";
|
import Terminal from "@components/Terminal";
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
import Modal from "../components/Modal";
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
|
@ -131,9 +133,6 @@ export default function KvmIdRoute() {
|
||||||
setModalView,
|
setModalView,
|
||||||
} = useUpdateStore();
|
} = useUpdateStore();
|
||||||
|
|
||||||
const [isOtherSessionConnectedModalOpen, setIsOtherSessionConnectedModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const sdp = useCallback(
|
const sdp = useCallback(
|
||||||
async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
|
async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
|
||||||
if (!pc) return;
|
if (!pc) return;
|
||||||
|
@ -243,8 +242,7 @@ export default function KvmIdRoute() {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We don't want to connect if another session is connected
|
if (location.pathname.includes("other-session")) return;
|
||||||
if (isOtherSessionConnectedModalOpen) return;
|
|
||||||
connectWebRTC();
|
connectWebRTC();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
@ -334,7 +332,7 @@ export default function KvmIdRoute() {
|
||||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||||
if (resp.method === "otherSessionConnected") {
|
if (resp.method === "otherSessionConnected") {
|
||||||
console.log("otherSessionConnected", resp.params);
|
console.log("otherSessionConnected", resp.params);
|
||||||
setIsOtherSessionConnectedModalOpen(true);
|
navigate("other-session");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "usbState") {
|
if (resp.method === "usbState") {
|
||||||
|
@ -445,6 +443,8 @@ export default function KvmIdRoute() {
|
||||||
};
|
};
|
||||||
}, [kvmTerminal]);
|
}, [kvmTerminal]);
|
||||||
|
|
||||||
|
const outlet = useOutlet();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Transition show={!isUpdateDialogOpen && otaState.updating}>
|
<Transition show={!isUpdateDialogOpen && otaState.updating}>
|
||||||
|
@ -486,18 +486,16 @@ export default function KvmIdRoute() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UpdateDialog open={isUpdateDialogOpen} setOpen={setIsUpdateDialogOpen} />
|
|
||||||
<OtherSessionConnectedModal
|
|
||||||
open={isOtherSessionConnectedModalOpen}
|
|
||||||
setOpen={state => {
|
|
||||||
if (!state) connectWebRTC().then(r => r);
|
|
||||||
|
|
||||||
// It takes some time for the WebRTC connection to be established, so we wait a bit before closing the modal
|
<Modal
|
||||||
setTimeout(() => {
|
open={outlet !== null}
|
||||||
setIsOtherSessionConnectedModalOpen(state);
|
onClose={() => location.pathname !== "/other-session" && navigate("..")}
|
||||||
}, 1000);
|
>
|
||||||
}}
|
<Outlet context={{ connectWebRTC }} />
|
||||||
/>
|
</Modal>
|
||||||
|
|
||||||
|
<UpdateDialog open={isUpdateDialogOpen} setOpen={setIsUpdateDialogOpen} />
|
||||||
|
|
||||||
{kvmTerminal && (
|
{kvmTerminal && (
|
||||||
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue