feat(ui): Add other session handling route and modal

This commit is contained in:
Adam Shiervani 2025-02-25 23:34:25 +01:00
parent ba0c937e2a
commit a2652c5265
4 changed files with 84 additions and 24 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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" />
)} )}