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"> | ||||
|       <DialogBackdrop | ||||
|         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="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 | ||||
|             transition | ||||
|             className={cx( | ||||
|  | @ -30,9 +30,12 @@ export default function Modal({ | |||
|               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="w-full pointer-events-none" onClick={e => e.stopPropagation()}> | ||||
|                 <div | ||||
|                   className="pointer-events-none w-full" | ||||
|                   onClick={e => e.stopPropagation()} | ||||
|                 > | ||||
|                   {children} | ||||
|                 </div> | ||||
|               </div> | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import WelcomeLocalModeRoute from "./routes/welcome-local.mode"; | |||
| import WelcomeRoute from "./routes/welcome-local"; | ||||
| import WelcomeLocalPasswordRoute from "./routes/welcome-local.password"; | ||||
| import { CLOUD_API } from "./ui.config"; | ||||
| import OtherSessionRoute from "./routes/devices.$id.other-session"; | ||||
| 
 | ||||
| export const isOnDevice = import.meta.env.MODE === "device"; | ||||
| export const isInCloud = !isOnDevice; | ||||
|  | @ -75,7 +76,14 @@ if (isOnDevice) { | |||
|       errorElement: <ErrorBoundary />, | ||||
|       element: <DeviceRoute />, | ||||
|       loader: DeviceRoute.loader, | ||||
|       children: [ | ||||
|         { | ||||
|           path: "other-session", | ||||
|           element: <OtherSessionRoute />, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|       path: "/adopt", | ||||
|       element: <AdoptRoute />, | ||||
|  | @ -116,6 +124,12 @@ if (isOnDevice) { | |||
|               path: "devices/:id", | ||||
|               element: <DeviceRoute />, | ||||
|               loader: DeviceRoute.loader, | ||||
|               children: [ | ||||
|                 { | ||||
|                   path: "other-session", | ||||
|                   element: <OtherSessionRoute />, | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|             { | ||||
|               path: "devices/:id/deregister", | ||||
|  | @ -164,8 +178,8 @@ function ErrorBoundary() { | |||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-full h-full"> | ||||
|       <div className="flex items-center justify-center h-full"> | ||||
|     <div className="h-full w-full"> | ||||
|       <div className="flex h-full items-center justify-center"> | ||||
|         <div className="w-full max-w-2xl"> | ||||
|           <EmptyCard | ||||
|             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 { | ||||
|   LoaderFunctionArgs, | ||||
|   Outlet, | ||||
|   Params, | ||||
|   redirect, | ||||
|   useLoaderData, | ||||
|   useNavigate, | ||||
|   useOutlet, | ||||
|   useParams, | ||||
|   useSearchParams, | ||||
| } from "react-router-dom"; | ||||
|  | @ -34,9 +36,9 @@ import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard | |||
| import api from "../api"; | ||||
| import { DeviceStatus } from "./welcome-local"; | ||||
| import FocusTrap from "focus-trap-react"; | ||||
| import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal"; | ||||
| import Terminal from "@components/Terminal"; | ||||
| import { CLOUD_API, DEVICE_API } from "@/ui.config"; | ||||
| import Modal from "../components/Modal"; | ||||
| 
 | ||||
| interface LocalLoaderResp { | ||||
|   authMode: "password" | "noPassword" | null; | ||||
|  | @ -131,9 +133,6 @@ export default function KvmIdRoute() { | |||
|     setModalView, | ||||
|   } = useUpdateStore(); | ||||
| 
 | ||||
|   const [isOtherSessionConnectedModalOpen, setIsOtherSessionConnectedModalOpen] = | ||||
|     useState(false); | ||||
| 
 | ||||
|   const sdp = useCallback( | ||||
|     async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => { | ||||
|       if (!pc) return; | ||||
|  | @ -243,8 +242,7 @@ export default function KvmIdRoute() { | |||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     // We don't want to connect if another session is connected
 | ||||
|     if (isOtherSessionConnectedModalOpen) return; | ||||
|     if (location.pathname.includes("other-session")) return; | ||||
|     connectWebRTC(); | ||||
|   }, 3000); | ||||
| 
 | ||||
|  | @ -334,7 +332,7 @@ export default function KvmIdRoute() { | |||
|   function onJsonRpcRequest(resp: JsonRpcRequest) { | ||||
|     if (resp.method === "otherSessionConnected") { | ||||
|       console.log("otherSessionConnected", resp.params); | ||||
|       setIsOtherSessionConnectedModalOpen(true); | ||||
|       navigate("other-session"); | ||||
|     } | ||||
| 
 | ||||
|     if (resp.method === "usbState") { | ||||
|  | @ -445,6 +443,8 @@ export default function KvmIdRoute() { | |||
|     }; | ||||
|   }, [kvmTerminal]); | ||||
| 
 | ||||
|   const outlet = useOutlet(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Transition show={!isUpdateDialogOpen && otaState.updating}> | ||||
|  | @ -486,18 +486,16 @@ export default function KvmIdRoute() { | |||
|           </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
 | ||||
|           setTimeout(() => { | ||||
|             setIsOtherSessionConnectedModalOpen(state); | ||||
|           }, 1000); | ||||
|         }} | ||||
|       /> | ||||
|       <Modal | ||||
|         open={outlet !== null} | ||||
|         onClose={() => location.pathname !== "/other-session" && navigate("..")} | ||||
|       > | ||||
|         <Outlet context={{ connectWebRTC }} /> | ||||
|       </Modal> | ||||
| 
 | ||||
|       <UpdateDialog open={isUpdateDialogOpen} setOpen={setIsUpdateDialogOpen} /> | ||||
| 
 | ||||
|       {kvmTerminal && ( | ||||
|         <Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" /> | ||||
|       )} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue