Merge pull request #14 from Seneca-CDOT/vnc-with-scaling
Add vnc connection and UI inprovementsmain
commit
933bbfa155
32 changed files with 1571 additions and 706 deletions
@ -0,0 +1,239 @@ |
||||
import { useState, useRef, useEffect, Dispatch, SetStateAction } from 'react'; |
||||
import dynamic from 'next/dynamic'; |
||||
import { Box, Menu, MenuItem, Typography, Button } from '@material-ui/core'; |
||||
import { makeStyles } from '@material-ui/core/styles'; |
||||
import CloseIcon from '@material-ui/icons/Close'; |
||||
import KeyboardIcon from '@material-ui/icons/Keyboard'; |
||||
import IconButton from '@material-ui/core/IconButton'; |
||||
import RFB from '@novnc/novnc/core/rfb'; |
||||
import { Panel } from '../Panels'; |
||||
import { BLACK, RED, TEXT } from '../../lib/consts/DEFAULT_THEME'; |
||||
import keyCombinations from './keyCombinations'; |
||||
import putFetch from '../../lib/fetchers/putFetch'; |
||||
import putFetchWithTimeout from '../../lib/fetchers/putFetchWithTimeout'; |
||||
import { HeaderText } from '../Text'; |
||||
import Spinner from '../Spinner'; |
||||
|
||||
const VncDisplay = dynamic(() => import('./VncDisplay'), { ssr: false }); |
||||
|
||||
const useStyles = makeStyles(() => ({ |
||||
displayBox: { |
||||
width: '75vw', |
||||
height: '75vh', |
||||
paddingTop: '1em', |
||||
paddingBottom: 0, |
||||
paddingLeft: 0, |
||||
paddingRight: 0, |
||||
}, |
||||
spinnerBox: { |
||||
flexDirection: 'column', |
||||
width: '75vw', |
||||
height: '75vh', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}, |
||||
closeButton: { |
||||
borderRadius: 8, |
||||
backgroundColor: RED, |
||||
'&:hover': { |
||||
backgroundColor: RED, |
||||
}, |
||||
}, |
||||
keyboardButton: { |
||||
borderRadius: 8, |
||||
backgroundColor: TEXT, |
||||
'&:hover': { |
||||
backgroundColor: TEXT, |
||||
}, |
||||
}, |
||||
closeBox: { |
||||
paddingBottom: '1em', |
||||
paddingLeft: '.7em', |
||||
paddingRight: 0, |
||||
}, |
||||
buttonsBox: { |
||||
paddingTop: 0, |
||||
}, |
||||
keysItem: { |
||||
backgroundColor: TEXT, |
||||
paddingRight: '3em', |
||||
'&:hover': { |
||||
backgroundColor: TEXT, |
||||
}, |
||||
}, |
||||
buttonText: { |
||||
color: BLACK, |
||||
}, |
||||
})); |
||||
|
||||
interface PreviewProps { |
||||
setMode: Dispatch<SetStateAction<boolean>>; |
||||
uuid: string; |
||||
serverName: string | string[] | undefined; |
||||
} |
||||
|
||||
interface VncConnectionProps { |
||||
protocol: string; |
||||
forward_port: number; |
||||
} |
||||
|
||||
const FullSize = ({ setMode, uuid, serverName }: PreviewProps): JSX.Element => { |
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); |
||||
const rfb = useRef<typeof RFB>(); |
||||
const hostname = useRef<string | undefined>(undefined); |
||||
const [vncConnection, setVncConnection] = useState< |
||||
VncConnectionProps | undefined |
||||
>(undefined); |
||||
const [isError, setIsError] = useState<boolean>(false); |
||||
const classes = useStyles(); |
||||
|
||||
useEffect(() => { |
||||
if (typeof window !== 'undefined') { |
||||
hostname.current = window.location.hostname; |
||||
} |
||||
|
||||
if (!vncConnection) |
||||
(async () => { |
||||
try { |
||||
const res = await putFetchWithTimeout( |
||||
`${process.env.NEXT_PUBLIC_API_URL}/manage_vnc_pipes`, |
||||
{ |
||||
server_uuid: uuid, |
||||
is_open: true, |
||||
}, |
||||
120000, |
||||
); |
||||
setVncConnection(await res.json()); |
||||
} catch { |
||||
setIsError(true); |
||||
} |
||||
})(); |
||||
}, [uuid, vncConnection, isError]); |
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => { |
||||
setAnchorEl(event.currentTarget); |
||||
}; |
||||
|
||||
const handleClickClose = async () => { |
||||
await putFetch(`${process.env.NEXT_PUBLIC_API_URL}/manage_vnc_pipes`, { |
||||
server_uuid: uuid, |
||||
is_open: false, |
||||
}); |
||||
}; |
||||
|
||||
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> |
||||
<Box flexGrow={1}> |
||||
<HeaderText text={`Server: ${serverName}`} /> |
||||
</Box> |
||||
{vncConnection ? ( |
||||
<Box display="flex" className={classes.displayBox}> |
||||
<VncDisplay |
||||
rfb={rfb} |
||||
url={`${vncConnection.protocol}://${hostname.current}:${vncConnection.forward_port}`} |
||||
viewOnly={false} |
||||
focusOnClick={false} |
||||
clipViewport={false} |
||||
dragViewport={false} |
||||
scaleViewport |
||||
resizeSession |
||||
showDotCursor={false} |
||||
background="" |
||||
qualityLevel={6} |
||||
compressionLevel={2} |
||||
/> |
||||
<Box> |
||||
<Box className={classes.closeBox}> |
||||
<IconButton |
||||
className={classes.closeButton} |
||||
style={{ color: TEXT }} |
||||
component="span" |
||||
onClick={() => { |
||||
handleClickClose(); |
||||
setMode(true); |
||||
}} |
||||
> |
||||
<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 }) => { |
||||
return ( |
||||
<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> |
||||
<Button |
||||
variant="contained" |
||||
onClick={() => { |
||||
setIsError(false); |
||||
}} |
||||
style={{ textTransform: 'none' }} |
||||
> |
||||
<Typography className={classes.buttonText} variant="subtitle1"> |
||||
Reconnect |
||||
</Typography> |
||||
</Button> |
||||
</> |
||||
)} |
||||
</Box> |
||||
)} |
||||
</Panel> |
||||
); |
||||
}; |
||||
|
||||
export default FullSize; |
@ -0,0 +1,79 @@ |
||||
import { Dispatch, SetStateAction } from 'react'; |
||||
import { Box } from '@material-ui/core'; |
||||
import { makeStyles } from '@material-ui/core/styles'; |
||||
import IconButton from '@material-ui/core/IconButton'; |
||||
import DesktopWindowsIcon from '@material-ui/icons/DesktopWindows'; |
||||
import CropOriginal from '@material-ui/icons/Image'; |
||||
import { Panel } from '../Panels'; |
||||
import { BLACK, GREY, TEXT } from '../../lib/consts/DEFAULT_THEME'; |
||||
import { HeaderText } from '../Text'; |
||||
|
||||
interface PreviewProps { |
||||
setMode: Dispatch<SetStateAction<boolean>>; |
||||
serverName: string | string[] | undefined; |
||||
} |
||||
|
||||
const useStyles = makeStyles(() => ({ |
||||
displayBox: { |
||||
padding: 0, |
||||
paddingTop: '.7em', |
||||
width: '100%', |
||||
}, |
||||
fullScreenButton: { |
||||
borderRadius: 8, |
||||
backgroundColor: TEXT, |
||||
'&:hover': { |
||||
backgroundColor: TEXT, |
||||
}, |
||||
}, |
||||
fullScreenBox: { |
||||
paddingLeft: '1em', |
||||
padding: 0, |
||||
}, |
||||
imageButton: { |
||||
padding: 0, |
||||
color: TEXT, |
||||
}, |
||||
imageIcon: { |
||||
borderRadius: 8, |
||||
padding: 0, |
||||
backgroundColor: GREY, |
||||
fontSize: '8em', |
||||
}, |
||||
})); |
||||
|
||||
const Preview = ({ setMode, serverName }: PreviewProps): JSX.Element => { |
||||
const classes = useStyles(); |
||||
|
||||
return ( |
||||
<Panel> |
||||
<Box flexGrow={1}> |
||||
<HeaderText text={`Server: ${serverName}`} /> |
||||
</Box> |
||||
<Box display="flex" className={classes.displayBox}> |
||||
<Box> |
||||
<IconButton |
||||
className={classes.imageButton} |
||||
style={{ color: BLACK }} |
||||
component="span" |
||||
onClick={() => setMode(false)} |
||||
> |
||||
<CropOriginal className={classes.imageIcon} /> |
||||
</IconButton> |
||||
</Box> |
||||
<Box className={classes.fullScreenBox}> |
||||
<IconButton |
||||
className={classes.fullScreenButton} |
||||
style={{ color: BLACK }} |
||||
component="span" |
||||
onClick={() => setMode(false)} |
||||
> |
||||
<DesktopWindowsIcon /> |
||||
</IconButton> |
||||
</Box> |
||||
</Box> |
||||
</Panel> |
||||
); |
||||
}; |
||||
|
||||
export default Preview; |
@ -0,0 +1,96 @@ |
||||
import { useEffect, useRef, MutableRefObject, memo } from 'react'; |
||||
import RFB from '@novnc/novnc/core/rfb'; |
||||
|
||||
type Props = { |
||||
rfb: MutableRefObject<typeof RFB | undefined>; |
||||
url: string; |
||||
viewOnly: boolean; |
||||
focusOnClick: boolean; |
||||
clipViewport: boolean; |
||||
dragViewport: boolean; |
||||
scaleViewport: boolean; |
||||
resizeSession: boolean; |
||||
showDotCursor: boolean; |
||||
background: string; |
||||
qualityLevel: number; |
||||
compressionLevel: number; |
||||
}; |
||||
|
||||
const VncDisplay = (props: Props): JSX.Element => { |
||||
const screen = useRef<HTMLDivElement>(null); |
||||
|
||||
const { |
||||
rfb, |
||||
url, |
||||
viewOnly, |
||||
focusOnClick, |
||||
clipViewport, |
||||
dragViewport, |
||||
scaleViewport, |
||||
resizeSession, |
||||
showDotCursor, |
||||
background, |
||||
qualityLevel, |
||||
compressionLevel, |
||||
} = props; |
||||
|
||||
useEffect(() => { |
||||
if (!screen.current) { |
||||
return (): void => { |
||||
if (rfb.current) { |
||||
rfb?.current.disconnect(); |
||||
rfb.current = undefined; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
if (!rfb.current) { |
||||
screen.current.innerHTML = ''; |
||||
|
||||
rfb.current = new RFB(screen.current, url); |
||||
|
||||
rfb.current.viewOnly = viewOnly; |
||||
rfb.current.focusOnClick = focusOnClick; |
||||
rfb.current.clipViewport = clipViewport; |
||||
rfb.current.dragViewport = dragViewport; |
||||
rfb.current.resizeSession = resizeSession; |
||||
rfb.current.scaleViewport = scaleViewport; |
||||
rfb.current.showDotCursor = showDotCursor; |
||||
rfb.current.background = background; |
||||
rfb.current.qualityLevel = qualityLevel; |
||||
rfb.current.compressionLevel = compressionLevel; |
||||
} |
||||
|
||||
/* eslint-disable consistent-return */ |
||||
if (!rfb.current) return; |
||||
|
||||
return (): void => { |
||||
if (rfb.current) { |
||||
rfb.current.disconnect(); |
||||
rfb.current = undefined; |
||||
} |
||||
}; |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rfb]); |
||||
|
||||
const handleMouseEnter = () => { |
||||
if ( |
||||
document.activeElement && |
||||
document.activeElement instanceof HTMLElement |
||||
) { |
||||
document.activeElement.blur(); |
||||
} |
||||
|
||||
if (rfb?.current) rfb.current.focus(); |
||||
}; |
||||
|
||||
return ( |
||||
<div |
||||
style={{ width: '100%', height: '75vh' }} |
||||
ref={screen} |
||||
onMouseEnter={handleMouseEnter} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default memo(VncDisplay); |
@ -0,0 +1,4 @@ |
||||
import FullSize from './FullSize'; |
||||
import Preview from './Preview'; |
||||
|
||||
export { FullSize, Preview }; |
@ -0,0 +1,27 @@ |
||||
const ControlL = '0xffe3'; |
||||
const AltL = '0xffe9'; |
||||
|
||||
const F1 = '0xffbe'; |
||||
const F2 = '0xffbf'; |
||||
const F3 = '0xffc0'; |
||||
const F4 = '0xffc1'; |
||||
const F5 = '0xffc2'; |
||||
const F6 = '0xffc3'; |
||||
const F7 = '0xffc4'; |
||||
const F8 = '0xffc5'; |
||||
const F9 = '0xffc6'; |
||||
|
||||
const keyCombinations: Array<{ keys: string; scans: string[] }> = [ |
||||
{ keys: 'Ctrl + Alt + Delete', scans: [] }, |
||||
{ keys: 'Ctrl + Alt + F1', scans: [ControlL, AltL, F1] }, |
||||
{ keys: 'Ctrl + Alt + F2', scans: [ControlL, AltL, F2] }, |
||||
{ keys: 'Ctrl + Alt + F3', scans: [ControlL, AltL, F3] }, |
||||
{ keys: 'Ctrl + Alt + F4', scans: [ControlL, AltL, F4] }, |
||||
{ keys: 'Ctrl + Alt + F5', scans: [ControlL, AltL, F5] }, |
||||
{ keys: 'Ctrl + Alt + F6', scans: [ControlL, AltL, F6] }, |
||||
{ keys: 'Ctrl + Alt + F7', scans: [ControlL, AltL, F7] }, |
||||
{ keys: 'Ctrl + Alt + F8', scans: [ControlL, AltL, F8] }, |
||||
{ keys: 'Ctrl + Alt + F9', scans: [ControlL, AltL, F9] }, |
||||
]; |
||||
|
||||
export default keyCombinations; |
@ -0,0 +1,12 @@ |
||||
import { Panel } from './Panels'; |
||||
import { HeaderText } from './Text'; |
||||
|
||||
const Domain = (): JSX.Element => { |
||||
return ( |
||||
<Panel> |
||||
<HeaderText text="Domain Settings" /> |
||||
</Panel> |
||||
); |
||||
}; |
||||
|
||||
export default Domain; |
@ -0,0 +1,131 @@ |
||||
import * as prettyBytes from 'pretty-bytes'; |
||||
import { makeStyles, Box, Divider } from '@material-ui/core'; |
||||
import InsertLinkIcon from '@material-ui/icons/InsertLink'; |
||||
import { InnerPanel, PanelHeader } from '../Panels'; |
||||
import { BodyText } from '../Text'; |
||||
import Decorator, { Colours } from '../Decorator'; |
||||
import { DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||
|
||||
const useStyles = makeStyles((theme) => ({ |
||||
root: { |
||||
overflow: 'auto', |
||||
height: '100%', |
||||
paddingLeft: '.3em', |
||||
[theme.breakpoints.down('md')]: { |
||||
overflow: 'hidden', |
||||
}, |
||||
}, |
||||
connection: { |
||||
paddingLeft: '.7em', |
||||
paddingRight: '.7em', |
||||
paddingTop: '1em', |
||||
paddingBottom: '.7em', |
||||
}, |
||||
bar: { |
||||
paddingLeft: '.7em', |
||||
paddingRight: '.7em', |
||||
}, |
||||
header: { |
||||
paddingTop: '.3em', |
||||
paddingRight: '.7em', |
||||
}, |
||||
label: { |
||||
paddingTop: '.3em', |
||||
}, |
||||
decoratorBox: { |
||||
paddingRight: '.3em', |
||||
}, |
||||
divider: { |
||||
background: DIVIDER, |
||||
}, |
||||
})); |
||||
|
||||
const selectDecorator = (state: string): Colours => { |
||||
switch (state) { |
||||
case 'connected': |
||||
return 'ok'; |
||||
case 'connecting': |
||||
return 'warning'; |
||||
default: |
||||
return 'error'; |
||||
} |
||||
}; |
||||
|
||||
const ResourceVolumes = ({ |
||||
resource, |
||||
}: { |
||||
resource: AnvilReplicatedStorage; |
||||
}): JSX.Element => { |
||||
const classes = useStyles(); |
||||
|
||||
return ( |
||||
<Box className={classes.root}> |
||||
{resource && |
||||
resource.volumes.map((volume) => { |
||||
return ( |
||||
<InnerPanel key={volume.drbd_device_minor}> |
||||
<PanelHeader> |
||||
<Box display="flex" width="100%" className={classes.header}> |
||||
<Box flexGrow={1}> |
||||
<BodyText text={`Volume: ${volume.number}`} /> |
||||
</Box> |
||||
<Box> |
||||
<BodyText |
||||
text={`Size: ${prettyBytes.default(volume.size, { |
||||
binary: true, |
||||
})}`}
|
||||
/> |
||||
</Box> |
||||
</Box> |
||||
</PanelHeader> |
||||
{volume.connections.map( |
||||
(connection, index): JSX.Element => { |
||||
return ( |
||||
<> |
||||
<Box |
||||
key={connection.fencing} |
||||
display="flex" |
||||
width="100%" |
||||
className={classes.connection} |
||||
> |
||||
<Box className={classes.decoratorBox}> |
||||
<Decorator |
||||
colour={selectDecorator( |
||||
connection.connection_state, |
||||
)} |
||||
/> |
||||
</Box> |
||||
<Box> |
||||
<Box display="flex" width="100%"> |
||||
<BodyText |
||||
text={connection.targets[0].target_name} |
||||
/> |
||||
<InsertLinkIcon style={{ color: DIVIDER }} /> |
||||
<BodyText |
||||
text={connection.targets[1].target_name} |
||||
/> |
||||
</Box> |
||||
<Box |
||||
display="flex" |
||||
justifyContent="center" |
||||
width="100%" |
||||
> |
||||
<BodyText text={connection.connection_state} /> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
{volume.connections.length - 1 !== index ? ( |
||||
<Divider className={classes.divider} /> |
||||
) : null} |
||||
</> |
||||
); |
||||
}, |
||||
)} |
||||
</InnerPanel> |
||||
); |
||||
})} |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default ResourceVolumes; |
@ -0,0 +1,18 @@ |
||||
import { Panel } from '../Panels'; |
||||
import { HeaderText } from '../Text'; |
||||
import ResourceVolumes from './ResourceVolumes'; |
||||
|
||||
const Resource = ({ |
||||
resource, |
||||
}: { |
||||
resource: AnvilReplicatedStorage; |
||||
}): JSX.Element => { |
||||
return ( |
||||
<Panel> |
||||
<HeaderText text={`Resource: ${resource.resource_name}`} /> |
||||
<ResourceVolumes resource={resource} /> |
||||
</Panel> |
||||
); |
||||
}; |
||||
|
||||
export default Resource; |
@ -0,0 +1,19 @@ |
||||
import { useEffect, useState } from 'react'; |
||||
|
||||
const useWindowDimensions = (): number | undefined => { |
||||
const [windowDimensions, setWindowDimensions] = useState<number | undefined>( |
||||
undefined, |
||||
); |
||||
useEffect(() => { |
||||
const handleResize = (): void => { |
||||
setWindowDimensions(window.innerWidth); |
||||
}; |
||||
handleResize(); |
||||
window.addEventListener('resize', handleResize); |
||||
return (): void => window.removeEventListener('resize', handleResize); |
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
|
||||
return windowDimensions; |
||||
}; |
||||
|
||||
export default useWindowDimensions; |
@ -0,0 +1,12 @@ |
||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||
const putFetch = <T>(uri: string, data: T): Promise<any> => { |
||||
return fetch(uri, { |
||||
method: 'PUT', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify(data), |
||||
}); |
||||
}; |
||||
|
||||
export default putFetch; |
@ -0,0 +1,25 @@ |
||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||
const putFetchTimeout = async <T>( |
||||
uri: string, |
||||
data: T, |
||||
timeout: number, |
||||
): Promise<any> => { |
||||
const controller = new AbortController(); |
||||
|
||||
const id = setTimeout(() => controller.abort(), timeout); |
||||
|
||||
const res = await fetch(uri, { |
||||
method: 'PUT', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
'Keep-Alive': 'timeout=120', |
||||
}, |
||||
signal: controller.signal, |
||||
body: JSON.stringify(data), |
||||
}); |
||||
clearTimeout(id); |
||||
|
||||
return res; |
||||
}; |
||||
|
||||
export default putFetchTimeout; |
@ -1,11 +0,0 @@ |
||||
const putJSON = <T>(uri: string, data: T): void => { |
||||
fetch(`${process.env.NEXT_PUBLIC_API_URL}${uri}`, { |
||||
method: 'PUT', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify(data), |
||||
}); |
||||
}; |
||||
|
||||
export default putJSON; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,57 @@ |
||||
import { useState } from 'react'; |
||||
import { useRouter } from 'next/router'; |
||||
import Head from 'next/head'; |
||||
import { Box } from '@material-ui/core'; |
||||
import { makeStyles } from '@material-ui/core/styles'; |
||||
|
||||
import { FullSize, Preview } from '../../components/Display'; |
||||
import Header from '../../components/Header'; |
||||
|
||||
const useStyles = makeStyles((theme) => ({ |
||||
preview: { |
||||
width: '25%', |
||||
height: '100%', |
||||
[theme.breakpoints.down('md')]: { |
||||
width: '100%', |
||||
}, |
||||
}, |
||||
fullView: { |
||||
display: 'flex', |
||||
flexDirection: 'row', |
||||
width: '100%', |
||||
justifyContent: 'center', |
||||
}, |
||||
})); |
||||
|
||||
const Server = (): JSX.Element => { |
||||
const [previewMode, setPreviewMode] = useState<boolean>(true); |
||||
const classes = useStyles(); |
||||
|
||||
const router = useRouter(); |
||||
const { uuid, server_name } = router.query; |
||||
|
||||
return ( |
||||
<> |
||||
<Head> |
||||
<title>{server_name}</title> |
||||
</Head> |
||||
<Header /> |
||||
{typeof uuid === 'string' && |
||||
(previewMode ? ( |
||||
<Box className={classes.preview}> |
||||
<Preview setMode={setPreviewMode} serverName={server_name} /> |
||||
</Box> |
||||
) : ( |
||||
<Box className={classes.fullView}> |
||||
<FullSize |
||||
setMode={setPreviewMode} |
||||
uuid={uuid} |
||||
serverName={server_name} |
||||
/> |
||||
</Box> |
||||
))} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default Server; |
@ -0,0 +1 @@ |
||||
declare module '@novnc/novnc/core/rfb'; |
Loading…
Reference in new issue