Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
8.1 KiB

import {
Close as CloseIcon,
Keyboard as KeyboardIcon,
} from '@mui/icons-material';
import {
Box,
IconButton,
IconButtonProps,
Menu,
MenuItem,
styled,
Typography,
} from '@mui/material';
import RFB from '@novnc/novnc/core/rfb';
import { useState, useRef, useEffect, FC, useCallback } from 'react';
import dynamic from 'next/dynamic';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import { BLACK, RED, TEXT } from '../../lib/consts/DEFAULT_THEME';
import ContainedButton from '../ContainedButton';
import { HeaderText } from '../Text';
import keyCombinations from './keyCombinations';
import { Panel } from '../Panels';
import putFetch from '../../lib/fetchers/putFetch';
import putFetchWithTimeout from '../../lib/fetchers/putFetchWithTimeout';
import Spinner from '../Spinner';
import useProtectedState from '../../hooks/useProtectedState';
const PREFIX = 'FullSize';
const classes = {
displayBox: `${PREFIX}-displayBox`,
spinnerBox: `${PREFIX}-spinnerBox`,
closeButton: `${PREFIX}-closeButton`,
keyboardButton: `${PREFIX}-keyboardButton`,
closeBox: `${PREFIX}-closeBox`,
buttonsBox: `${PREFIX}-buttonsBox`,
keysItem: `${PREFIX}-keysItem`,
};
const StyledDiv = styled('div')(() => ({
[`& .${classes.displayBox}`]: {
width: '75vw',
height: '75vh',
paddingTop: '1em',
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
},
[`& .${classes.spinnerBox}`]: {
flexDirection: 'column',
width: '75vw',
height: '75vh',
alignItems: 'center',
justifyContent: 'center',
},
[`& .${classes.closeButton}`]: {
borderRadius: 8,
backgroundColor: RED,
'&:hover': {
backgroundColor: RED,
},
},
[`& .${classes.keyboardButton}`]: {
borderRadius: 8,
backgroundColor: TEXT,
'&:hover': {
backgroundColor: TEXT,
},
},
[`& .${classes.closeBox}`]: {
paddingBottom: '1em',
paddingLeft: '.7em',
paddingRight: 0,
},
[`& .${classes.buttonsBox}`]: {
paddingTop: 0,
},
[`& .${classes.keysItem}`]: {
backgroundColor: TEXT,
paddingRight: '3em',
'&:hover': {
backgroundColor: TEXT,
},
},
}));
const CMD_VNC_PIPE_URL = `${API_BASE_URL}/command/vnc-pipe`;
const VncDisplay = dynamic(() => import('./VncDisplay'), { ssr: false });
type FullSizeOptionalProps = {
onClickCloseButton?: IconButtonProps['onClick'];
};
type FullSizeProps = FullSizeOptionalProps & {
serverUUID: string;
serverName: string | string[] | undefined;
};
type VncConnectionProps = {
protocol: string;
forwardPort: number;
};
const FULL_SIZE_DEFAULT_PROPS: Required<
Omit<FullSizeOptionalProps, 'onClickCloseButton'>
> &
Pick<FullSizeOptionalProps, 'onClickCloseButton'> = {
onClickCloseButton: undefined,
};
const FullSize: FC<FullSizeProps> = ({
onClickCloseButton,
serverUUID,
serverName,
}): JSX.Element => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const rfb = useRef<typeof RFB>();
const hostname = useRef<string | undefined>(undefined);
const [vncConnection, setVncConnection] = useProtectedState<
VncConnectionProps | undefined
>(undefined);
const [vncConnecting, setVncConnecting] = useProtectedState<boolean>(false);
const [isError, setIsError] = useProtectedState<boolean>(false);
const connectVnc = useCallback(async () => {
if (vncConnection || vncConnecting) return;
setVncConnecting(true);
try {
const res = await putFetchWithTimeout(
CMD_VNC_PIPE_URL,
{
serverUuid: serverUUID,
open: true,
},
120000,
);
setVncConnection(await res.json());
} catch {
setIsError(true);
} finally {
setVncConnecting(false);
}
}, [
serverUUID,
setIsError,
setVncConnecting,
setVncConnection,
vncConnecting,
vncConnection,
]);
useEffect(() => {
if (typeof window !== 'undefined') {
hostname.current = window.location.hostname;
}
connectVnc();
}, [connectVnc]);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
setAnchorEl(event.currentTarget);
};
const handleClickClose = async () => {
await putFetch(CMD_VNC_PIPE_URL, { serverUuid: serverUUID });
};
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);
}
};
return (
<Panel>
<StyledDiv>
<Box flexGrow={1}>
<HeaderText text={`Server: ${serverName}`} />
</Box>
{vncConnection ? (
<Box display="flex" className={classes.displayBox}>
<VncDisplay
rfb={rfb}
url={`${vncConnection.protocol}://${hostname.current}:${vncConnection.forwardPort}`}
viewOnly={false}
focusOnClick={false}
clipViewport={false}
dragViewport={false}
scaleViewport
resizeSession
showDotCursor={false}
background=""
qualityLevel={6}
compressionLevel={2}
onDisconnect={({ detail: { clean } }) => {
if (!clean) {
setVncConnection(undefined);
connectVnc();
}
}}
/>
<Box>
<Box className={classes.closeBox}>
<IconButton
className={classes.closeButton}
style={{ color: TEXT }}
component="span"
onClick={(
...args: Parameters<
Exclude<IconButtonProps['onClick'], undefined>
>
) => {
handleClickClose();
onClickCloseButton?.call(null, ...args);
}}
>
<CloseIcon />
</IconButton>
</Box>
<Box className={classes.closeBox}>
<IconButton
className={classes.keyboardButton}
style={{ color: BLACK }}
component="span"
onClick={handleClick}
>
<KeyboardIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{keyCombinations.map(({ keys, scans }) => (
<MenuItem
onClick={() => handleSendKeys(scans)}
className={classes.keysItem}
key={keys}
>
<Typography variant="subtitle1">{keys}</Typography>
</MenuItem>
))}
</Menu>
</Box>
</Box>
</Box>
) : (
<Box display="flex" className={classes.spinnerBox}>
{!isError ? (
<>
<HeaderText
text={`Establishing connection with ${serverName}`}
/>
<HeaderText text="This may take a few minutes" />
<Spinner />
</>
) : (
<>
<Box style={{ paddingBottom: '2em' }}>
<HeaderText text="There was a problem connecting to the server, please try again" />
</Box>
<ContainedButton
onClick={() => {
setIsError(false);
}}
>
Reconnect
</ContainedButton>
</>
)}
</Box>
)}
</StyledDiv>
</Panel>
);
};
FullSize.defaultProps = FULL_SIZE_DEFAULT_PROPS;
export default FullSize;