import { Close as CloseIcon, Dashboard as DashboardIcon, Fullscreen as FullscreenIcon, Keyboard as KeyboardIcon, } from '@mui/icons-material'; import { Box, Menu, styled, Typography } from '@mui/material'; import RFB from '@novnc/novnc/core/rfb'; import dynamic from 'next/dynamic'; import { useState, useEffect, FC, useMemo, useRef, useCallback } from 'react'; import IconButton from '../IconButton'; import keyCombinations from './keyCombinations'; import MenuItem from '../MenuItem'; import { Panel, PanelHeader } from '../Panels'; import ServerMenu from '../ServerMenu'; import Spinner from '../Spinner'; import { HeaderText } from '../Text'; import useIsFirstRender from '../../hooks/useIsFirstRender'; const PREFIX = 'FullSize'; const classes = { displayBox: `${PREFIX}-displayBox`, spinnerBox: `${PREFIX}-spinnerBox`, }; const StyledDiv = styled('div')(() => ({ [`& .${classes.displayBox}`]: { width: '75vw', height: '75vh', }, [`& .${classes.spinnerBox}`]: { flexDirection: 'column', width: '75vw', height: '75vh', alignItems: 'center', justifyContent: 'center', }, })); const VncDisplay = dynamic(() => import('./VncDisplay'), { ssr: false }); // Unit: seconds const DEFAULT_VNC_RECONNECT_TIMER_START = 5; const buildServerVncUrl = (host: string, serverUuid: string) => `ws://${host}/ws/server/vnc/${serverUuid}`; const FullSize: FC = ({ onClickCloseButton, serverUUID, serverName, vncReconnectTimerStart = DEFAULT_VNC_RECONNECT_TIMER_START, }): JSX.Element => { const isFirstRender = useIsFirstRender(); const [anchorEl, setAnchorEl] = useState(null); const [rfbConnectArgs, setRfbConnectArgs] = useState< Partial | undefined >(undefined); const [vncConnecting, setVncConnecting] = useState(false); const [vncError, setVncError] = useState(false); const [vncReconnectTimer, setVncReconnectTimer] = useState( vncReconnectTimerStart, ); const rfb = useRef(null); const rfbScreen = useRef(null); const handleClickKeyboard = ( event: React.MouseEvent, ): void => { setAnchorEl(event.currentTarget); }; const handleSendKeys = (scans: string[]) => { if (rfb.current) { if (!scans.length) rfb.current.sendCtrlAltDel(); else { // Send pressing keys for (let i = 0; i <= scans.length - 1; i += 1) { rfb.current.sendKey(scans[i], 1); } // Send releasing keys in reverse order for (let i = scans.length - 1; i >= 0; i -= 1) { rfb.current.sendKey(scans[i], 0); } } setAnchorEl(null); } }; const connectServerVnc = useCallback(() => { setVncConnecting(true); setVncError(false); setRfbConnectArgs({ url: buildServerVncUrl(window.location.host, serverUUID), }); }, [serverUUID]); const disconnectServerVnc = useCallback(() => { if (rfb?.current) { rfb.current.disconnect(); rfb.current = null; } setRfbConnectArgs(undefined); }, []); const reconnectServerVnc = useCallback(() => { disconnectServerVnc(); connectServerVnc(); }, [connectServerVnc, disconnectServerVnc]); const updateVncReconnectTimer = useCallback((): void => { const intervalId = setInterval((): void => { setVncReconnectTimer((previous) => { const current = previous - 1; if (current < 1) { clearInterval(intervalId); } return current; }); }, 1000); }, []); // 'connect' event emits when a connection successfully completes. const rfbConnectEventHandler = useCallback(() => { setVncConnecting(false); }, []); // 'disconnect' event emits when a connection fails, // OR when a user closes the existing connection. const rfbDisconnectEventHandler = useCallback( ({ detail: { clean } }) => { if (!clean) { setVncConnecting(false); setVncError(true); updateVncReconnectTimer(); } }, [updateVncReconnectTimer], ); const showScreen = useMemo( () => !vncConnecting && !vncError, [vncConnecting, vncError], ); const fullscreenElement = useMemo( () => ( { rfbScreen.current?.requestFullscreen(); }} > ), [], ); const keyboardMenuElement = useMemo( () => ( setAnchorEl(null)} > {keyCombinations.map(({ keys, scans }) => ( handleSendKeys(scans)}> {keys} ))} ), [anchorEl], ); const vncDisconnectElement = useMemo( () => ( { disconnectServerVnc(); onClickCloseButton?.call(null, ...args); }} > ), [disconnectServerVnc, onClickCloseButton], ); const returnHomeElement = useMemo( () => ( { if (!window) return; disconnectServerVnc(); window.location.assign('/'); }} > ), [disconnectServerVnc], ); const vncToolbarElement = useMemo( () => showScreen && ( <> {fullscreenElement} {keyboardMenuElement} {returnHomeElement} {vncDisconnectElement} ), [ fullscreenElement, keyboardMenuElement, returnHomeElement, serverName, serverUUID, showScreen, vncDisconnectElement, ], ); useEffect(() => { if (vncReconnectTimer === 0) { setVncReconnectTimer(vncReconnectTimerStart); reconnectServerVnc(); } }, [reconnectServerVnc, vncReconnectTimer, vncReconnectTimerStart]); useEffect(() => { if (isFirstRender) { connectServerVnc(); } }, [connectServerVnc, isFirstRender]); return ( {vncToolbarElement} {!showScreen && ( {vncConnecting && ( <> Connecting to {serverName}. )} {vncError && ( <> There was a problem connecting to the server. Retrying in {vncReconnectTimer}. )} )} ); }; export default FullSize;