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 { BodyText, HeaderText } from '../Text'; import useCookieJar from '../../hooks/useCookieJar'; 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 = 10; const MAP_TO_WSCODE_MSG: Record = { 1000: 'in-use by another process?', 1006: 'destination is down?', }; 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 { buildCookieJar } = useCookieJar(); 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 [vncWsErrorMessage, setVncWsErrorMessage] = useState< string | undefined >(); const [vncApiErrorMessage, setVncApiErrorMessage] = useState< string | undefined >(); 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( (event) => { const { detail } = event; const { clean } = detail; if (clean) return; setVncConnecting(false); setVncError(true); updateVncReconnectTimer(); }, [updateVncReconnectTimer], ); const wsCloseEventHandler = useCallback( (event?: WebsockCloseEvent): void => { if (!event) { setVncWsErrorMessage(undefined); return; } const { code: wscode, reason } = event; let wsmsg = `ws: ${wscode}`; const guess = MAP_TO_WSCODE_MSG[wscode]; if (guess) { wsmsg += ` (${guess})`; } if (reason) { wsmsg += `, ${reason}`; } setVncWsErrorMessage(wsmsg); const vncerror = buildCookieJar()[ `suiapi.vncerror.${serverUUID}` ] as APIError; if (!vncerror) { setVncApiErrorMessage(undefined); return; } const { code: apicode, message } = vncerror; setVncApiErrorMessage(`api: ${apicode}, ${message}`); }, [buildCookieJar, serverUUID], ); 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 && ( <> Can't connect to the server. {vncApiErrorMessage} {vncWsErrorMessage} Retrying in {vncReconnectTimer}. )} )} ); }; export default FullSize;