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