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