Merge pull request #14 from Seneca-CDOT/vnc-with-scaling

Add vnc connection and UI inprovements
main
つばめ 3 years ago committed by GitHub
commit 933bbfa155
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      striker-ui/.eslintrc.json
  2. 20
      striker-ui/components/AnvilDrawer.tsx
  3. 9
      striker-ui/components/Anvils/AnvilList.tsx
  4. 4
      striker-ui/components/Anvils/SelectedAnvil.tsx
  5. 239
      striker-ui/components/Display/FullSize.tsx
  6. 79
      striker-ui/components/Display/Preview.tsx
  7. 96
      striker-ui/components/Display/VncDisplay.tsx
  8. 4
      striker-ui/components/Display/index.tsx
  9. 27
      striker-ui/components/Display/keyCombinations.ts
  10. 12
      striker-ui/components/Domain.tsx
  11. 3
      striker-ui/components/FileSystem/FileSystems.tsx
  12. 21
      striker-ui/components/Header.tsx
  13. 20
      striker-ui/components/Hosts/AnvilHost.tsx
  14. 8
      striker-ui/components/Network/Network.tsx
  15. 2
      striker-ui/components/Panels/InnerPanel.tsx
  16. 13
      striker-ui/components/Panels/Panel.tsx
  17. 131
      striker-ui/components/Resource/ResourceVolumes.tsx
  18. 18
      striker-ui/components/Resource/index.tsx
  19. 11
      striker-ui/components/Servers.tsx
  20. 4
      striker-ui/components/SharedStorage/SharedStorage.tsx
  21. 19
      striker-ui/hooks/useWindowDimenions.ts
  22. 1
      striker-ui/lib/consts/DEFAULT_THEME.ts
  23. 5
      striker-ui/lib/consts/ICONS.ts
  24. 12
      striker-ui/lib/fetchers/putFetch.ts
  25. 25
      striker-ui/lib/fetchers/putFetchWithTimeout.ts
  26. 11
      striker-ui/lib/fetchers/putJSON.ts
  27. 1199
      striker-ui/package-lock.json
  28. 24
      striker-ui/package.json
  29. 34
      striker-ui/pages/index.tsx
  30. 57
      striker-ui/pages/server/index.tsx
  31. 22
      striker-ui/types/AnvilReplicatedStorage.d.ts
  32. 1
      striker-ui/types/novnc__novnc.d.ts

@ -52,7 +52,7 @@
"react/prop-types": "off", "react/prop-types": "off",
// Importing React is not required in Next.js // Importing React is not required in Next.js
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/jsx-curly-newline": "off",
"camelcase": "off", "camelcase": "off",
"@typescript-eslint/camelcase": "off" "@typescript-eslint/camelcase": "off"
}, },

@ -1,9 +1,10 @@
import { Divider, Drawer, List, ListItem, Box } from '@material-ui/core'; import { Divider, Drawer, List, ListItem, Box } from '@material-ui/core';
import { makeStyles, createStyles } from '@material-ui/core/styles'; import { makeStyles, createStyles } from '@material-ui/core/styles';
import DashboardIcon from '@material-ui/icons/Dashboard';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { BodyText, HeaderText } from './Text'; import { BodyText, HeaderText } from './Text';
import { ICONS, ICON_SIZE } from '../lib/consts/ICONS'; import { ICONS, ICON_SIZE } from '../lib/consts/ICONS';
import { DIVIDER } from '../lib/consts/DEFAULT_THEME'; import { DIVIDER, GREY } from '../lib/consts/DEFAULT_THEME';
interface DrawerProps { interface DrawerProps {
open: boolean; open: boolean;
@ -22,6 +23,13 @@ const useStyles = makeStyles(() =>
paddingTop: '.5em', paddingTop: '.5em',
paddingLeft: '1.5em', paddingLeft: '1.5em',
}, },
dashboardButton: {
paddingLeft: '.1em',
},
dashboardIcon: {
fontSize: '2.3em',
color: GREY,
},
}), }),
); );
@ -41,6 +49,16 @@ const AnvilDrawer = ({ open, setOpen }: DrawerProps): JSX.Element => {
<HeaderText text="Admin" /> <HeaderText text="Admin" />
</ListItem> </ListItem>
<Divider className={classes.divider} /> <Divider className={classes.divider} />
<ListItem button component="a" href="/index.html">
<Box display="flex" flexDirection="row" width="100%">
<Box className={classes.dashboardButton}>
<DashboardIcon className={classes.dashboardIcon} />
</Box>
<Box flexGrow={1} className={classes.text}>
<BodyText text="Dashboard" />
</Box>
</Box>
</ListItem>
{ICONS.map( {ICONS.map(
(icon): JSX.Element => ( (icon): JSX.Element => (
<ListItem <ListItem

@ -1,7 +1,11 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { List, Box, Divider, ListItem } from '@material-ui/core'; import { List, Box, Divider, ListItem } from '@material-ui/core';
import { HOVER, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; import {
HOVER,
DIVIDER,
LARGE_MOBILE_BREAKPOINT,
} from '../../lib/consts/DEFAULT_THEME';
import Anvil from './Anvil'; import Anvil from './Anvil';
import { AnvilContext } from '../AnvilContext'; import { AnvilContext } from '../AnvilContext';
import sortAnvils from './sortAnvils'; import sortAnvils from './sortAnvils';
@ -12,7 +16,8 @@ const useStyles = makeStyles((theme) => ({
width: '100%', width: '100%',
overflow: 'auto', overflow: 'auto',
height: '30vh', height: '30vh',
[theme.breakpoints.down('md')]: { paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
}, },

@ -6,7 +6,7 @@ import { SELECTED_ANVIL } from '../../lib/consts/DEFAULT_THEME';
import anvilState from '../../lib/consts/ANVILS'; import anvilState from '../../lib/consts/ANVILS';
import { AnvilContext } from '../AnvilContext'; import { AnvilContext } from '../AnvilContext';
import Decorator, { Colours } from '../Decorator'; import Decorator, { Colours } from '../Decorator';
import putJSON from '../../lib/fetchers/putJSON'; import putFetch from '../../lib/fetchers/putFetch';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
root: { root: {
@ -67,7 +67,7 @@ const SelectedAnvil = ({ list }: { list: AnvilListItem[] }): JSX.Element => {
<Switch <Switch
checked={isAnvilOn(list[index])} checked={isAnvilOn(list[index])}
onChange={() => onChange={() =>
putJSON('/set_power', { putFetch(`${process.env.NEXT_PUBLIC_API_URL}/set_power`, {
anvil_uuid: list[index].anvil_uuid, anvil_uuid: list[index].anvil_uuid,
is_on: !isAnvilOn(list[index]), is_on: !isAnvilOn(list[index]),
}) })

@ -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;

@ -8,6 +8,7 @@ import SharedStorageHost from './FileSystemsHost';
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; import PeriodicFetch from '../../lib/fetchers/periodicFetch';
import { AnvilContext } from '../AnvilContext'; import { AnvilContext } from '../AnvilContext';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import { LARGE_MOBILE_BREAKPOINT } from '../../lib/consts/DEFAULT_THEME';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
header: { header: {
@ -18,7 +19,7 @@ const useStyles = makeStyles((theme) => ({
overflow: 'auto', overflow: 'auto',
height: '78vh', height: '78vh',
paddingLeft: '.3em', paddingLeft: '.3em',
[theme.breakpoints.down('md')]: { [theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
}, },
}, },

@ -1,13 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import { makeStyles, createStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { Box, Button } from '@material-ui/core'; import { Box, Button } from '@material-ui/core';
import { ICONS, ICON_SIZE } from '../lib/consts/ICONS'; import { ICONS, ICON_SIZE } from '../lib/consts/ICONS';
import { BORDER_RADIUS, RED } from '../lib/consts/DEFAULT_THEME'; import { BORDER_RADIUS, RED } from '../lib/consts/DEFAULT_THEME';
import AnvilDrawer from './AnvilDrawer'; import AnvilDrawer from './AnvilDrawer';
const useStyles = makeStyles((theme) => const useStyles = makeStyles((theme) => ({
createStyles({
appBar: { appBar: {
paddingTop: theme.spacing(0.5), paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5), paddingBottom: theme.spacing(0.5),
@ -25,7 +24,7 @@ const useStyles = makeStyles((theme) =>
barElement: { barElement: {
padding: 0, padding: 0,
}, },
icons: { iconBox: {
[theme.breakpoints.down('sm')]: { [theme.breakpoints.down('sm')]: {
display: 'none', display: 'none',
}, },
@ -36,8 +35,11 @@ const useStyles = makeStyles((theme) =>
paddingLeft: '15vw', paddingLeft: '15vw',
}, },
}, },
}), icons: {
); paddingLeft: '.1em',
paddingRight: '.1em',
},
}));
const Header = (): JSX.Element => { const Header = (): JSX.Element => {
const classes = useStyles(); const classes = useStyles();
@ -46,7 +48,6 @@ const Header = (): JSX.Element => {
const toggleDrawer = (): void => setOpen(!open); const toggleDrawer = (): void => setOpen(!open);
return ( return (
<>
<AppBar position="static" className={classes.appBar}> <AppBar position="static" className={classes.appBar}>
<Box display="flex" justifyContent="space-between" flexDirection="row"> <Box display="flex" justifyContent="space-between" flexDirection="row">
<Box className={classes.barElement}> <Box className={classes.barElement}>
@ -54,7 +55,7 @@ const Header = (): JSX.Element => {
<img alt="" src="/pngs/logo.png" width="160" height="40" /> <img alt="" src="/pngs/logo.png" width="160" height="40" />
</Button> </Button>
</Box> </Box>
<Box className={`${classes.barElement} ${classes.icons}`}> <Box className={`${classes.barElement} ${classes.iconBox}`}>
{ICONS.map( {ICONS.map(
(icon): JSX.Element => ( (icon): JSX.Element => (
<a <a
@ -71,15 +72,15 @@ const Header = (): JSX.Element => {
src={icon.image} src={icon.image}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...ICON_SIZE} {...ICON_SIZE}
className={classes.icons}
/> />
</a> </a>
), ),
)} )}
</Box> </Box>
</Box> </Box>
</AppBar>
<AnvilDrawer open={open} setOpen={setOpen} /> <AnvilDrawer open={open} setOpen={setOpen} />
</> </AppBar>
); );
}; };

@ -6,14 +6,16 @@ import { BodyText } from '../Text';
import Decorator, { Colours } from '../Decorator'; import Decorator, { Colours } from '../Decorator';
import HOST_STATUS from '../../lib/consts/NODES'; import HOST_STATUS from '../../lib/consts/NODES';
import putJSON from '../../lib/fetchers/putJSON'; import putFetch from '../../lib/fetchers/putFetch';
import { LARGE_MOBILE_BREAKPOINT } from '../../lib/consts/DEFAULT_THEME';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
overflow: 'auto', overflow: 'auto',
height: '28vh', height: '28vh',
paddingLeft: '.3em', paddingLeft: '.3em',
[theme.breakpoints.down('md')]: { paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
}, },
@ -102,10 +104,13 @@ const AnvilHost = ({
<Switch <Switch
checked={host.state === 'online'} checked={host.state === 'online'}
onChange={() => onChange={() =>
putJSON('/set_power', { putFetch(
`${process.env.NEXT_PUBLIC_API_URL}/set_power`,
{
host_uuid: host.host_uuid, host_uuid: host.host_uuid,
is_on: !(host.state === 'online'), is_on: !(host.state === 'online'),
}) },
)
} }
/> />
</Box> </Box>
@ -117,10 +122,13 @@ const AnvilHost = ({
checked={host.state === 'online'} checked={host.state === 'online'}
disabled={!(host.state === 'online')} disabled={!(host.state === 'online')}
onChange={() => onChange={() =>
putJSON('/set_membership', { putFetch(
`${process.env.NEXT_PUBLIC_API_URL}/set_membership`,
{
host_uuid: host.host_uuid, host_uuid: host.host_uuid,
is_member: !(host.state === 'online'), is_member: !(host.state === 'online'),
}) },
)
} }
/> />
</Box> </Box>

@ -4,7 +4,10 @@ import { makeStyles } from '@material-ui/core/styles';
import { Panel } from '../Panels'; import { Panel } from '../Panels';
import { HeaderText, BodyText } from '../Text'; import { HeaderText, BodyText } from '../Text';
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; import PeriodicFetch from '../../lib/fetchers/periodicFetch';
import { DIVIDER } from '../../lib/consts/DEFAULT_THEME'; import {
DIVIDER,
LARGE_MOBILE_BREAKPOINT,
} from '../../lib/consts/DEFAULT_THEME';
import processNetworkData from './processNetwork'; import processNetworkData from './processNetwork';
import { AnvilContext } from '../AnvilContext'; import { AnvilContext } from '../AnvilContext';
import Decorator, { Colours } from '../Decorator'; import Decorator, { Colours } from '../Decorator';
@ -15,7 +18,8 @@ const useStyles = makeStyles((theme) => ({
width: '100%', width: '100%',
overflow: 'auto', overflow: 'auto',
height: '32vh', height: '32vh',
[theme.breakpoints.down('md')]: { paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
}, },

@ -15,7 +15,7 @@ const useStyles = makeStyles(() => ({
borderColor: DIVIDER, borderColor: DIVIDER,
marginTop: '1.4em', marginTop: '1.4em',
marginBottom: '1.4em', marginBottom: '1.4em',
paddingBottom: '.7em', paddingBottom: 0,
position: 'relative', position: 'relative',
}, },
})); }));

@ -42,6 +42,19 @@ const useStyles = makeStyles(() => ({
bottom: '-.3em', bottom: '-.3em',
right: '-.3em', right: '-.3em',
}, },
'@global': {
'*::-webkit-scrollbar': {
width: '.6em',
},
'*::-webkit-scrollbar-track': {
backgroundColor: PANEL_BACKGROUND,
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: TEXT,
outline: '1px solid transparent',
borderRadius: BORDER_RADIUS,
},
},
})); }));
const Panel = ({ children }: Props): JSX.Element => { const Panel = ({ children }: Props): JSX.Element => {

@ -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;

@ -26,6 +26,7 @@ import {
RED, RED,
GREY, GREY,
BLACK, BLACK,
LARGE_MOBILE_BREAKPOINT,
} from '../lib/consts/DEFAULT_THEME'; } from '../lib/consts/DEFAULT_THEME';
import { AnvilContext } from './AnvilContext'; import { AnvilContext } from './AnvilContext';
import serverState from '../lib/consts/SERVERS'; import serverState from '../lib/consts/SERVERS';
@ -33,15 +34,17 @@ import Decorator, { Colours } from './Decorator';
import Spinner from './Spinner'; import Spinner from './Spinner';
import hostsSanitizer from '../lib/sanitizers/hostsSanitizer'; import hostsSanitizer from '../lib/sanitizers/hostsSanitizer';
import putJSON from '../lib/fetchers/putJSON'; import putFetch from '../lib/fetchers/putFetch';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
width: '100%', width: '100%',
overflow: 'auto', overflow: 'auto',
height: '78vh', height: '78vh',
[theme.breakpoints.down('md')]: { paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
overflow: 'hidden',
}, },
}, },
divider: { divider: {
@ -142,7 +145,7 @@ const Servers = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => {
const handlePower = (label: ButtonLabels) => { const handlePower = (label: ButtonLabels) => {
setAnchorEl(null); setAnchorEl(null);
if (selected.length) { if (selected.length) {
putJSON('/set_power', { putFetch(`${process.env.NEXT_PUBLIC_API_URL}/set_power`, {
server_uuid_list: selected, server_uuid_list: selected,
is_on: label === 'on', is_on: label === 'on',
}); });
@ -253,6 +256,8 @@ const Servers = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => {
button button
className={classes.button} className={classes.button}
key={server.server_uuid} key={server.server_uuid}
component="a"
href={`/server?uuid=${server.server_uuid}&server_name=${server.server_name}`}
> >
<Box display="flex" flexDirection="row" width="100%"> <Box display="flex" flexDirection="row" width="100%">
{showCheckbox && ( {showCheckbox && (

@ -8,6 +8,7 @@ import SharedStorageHost from './SharedStorageHost';
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; import PeriodicFetch from '../../lib/fetchers/periodicFetch';
import { AnvilContext } from '../AnvilContext'; import { AnvilContext } from '../AnvilContext';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import { LARGE_MOBILE_BREAKPOINT } from '../../lib/consts/DEFAULT_THEME';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
header: { header: {
@ -18,7 +19,8 @@ const useStyles = makeStyles((theme) => ({
overflow: 'auto', overflow: 'auto',
height: '78vh', height: '78vh',
paddingLeft: '.3em', paddingLeft: '.3em',
[theme.breakpoints.down('md')]: { paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%', height: '100%',
}, },
}, },

@ -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;

@ -15,3 +15,4 @@ export const DISABLED = '#AAA';
export const BLACK = '#343434'; export const BLACK = '#343434';
export const BORDER_RADIUS = '3px'; export const BORDER_RADIUS = '3px';
export const LARGE_MOBILE_BREAKPOINT = 1800;

@ -29,6 +29,11 @@ export const ICONS = [
image: '/pngs/email_on.png', image: '/pngs/email_on.png',
uri: '/striker?email=true', uri: '/striker?email=true',
}, },
{
text: 'Logout',
image: '/pngs/users_icon_on.png',
uri: '/striker?logout=true',
},
{ {
text: 'Help', text: 'Help',
image: '/pngs/help_icon_on.png', image: '/pngs/help_icon_on.png',

@ -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

@ -15,6 +15,7 @@
"@material-ui/core": "^4.11.3", "@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@novnc/novnc": "^1.2.0",
"next": "^10.2.3", "next": "^10.2.3",
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"react": "17.0.2", "react": "17.0.2",
@ -24,22 +25,23 @@
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^12.1.4", "@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^11.0.0", "@commitlint/config-conventional": "^12.1.4",
"@types/node": "^14.14.26", "@types/node": "^15.12.2",
"@types/novnc-core": "^0.1.3",
"@types/react": "^17.0.11", "@types/react": "^17.0.11",
"@types/styled-components": "^5.1.10", "@types/styled-components": "^5.1.10",
"@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.15.0", "@typescript-eslint/parser": "^4.27.0",
"eslint": "^7.19.0", "eslint": "^7.28.0",
"eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^7.2.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"husky": "^5.0.9", "husky": "^6.0.0",
"lint-staged": "^10.5.4", "lint-staged": "^11.0.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"typescript": "^4.1.5" "typescript": "^4.1.5"
} }

@ -1,3 +1,4 @@
import Head from 'next/head';
import { Box } from '@material-ui/core'; import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
@ -11,13 +12,15 @@ import PeriodicFetch from '../lib/fetchers/periodicFetch';
import Servers from '../components/Servers'; import Servers from '../components/Servers';
import Header from '../components/Header'; import Header from '../components/Header';
import AnvilProvider from '../components/AnvilContext'; import AnvilProvider from '../components/AnvilContext';
import { LARGE_MOBILE_BREAKPOINT } from '../lib/consts/DEFAULT_THEME';
import useWindowDimensions from '../hooks/useWindowDimenions';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
child: { child: {
width: '22%', width: '22%',
height: '100%', height: '100%',
[theme.breakpoints.down('lg')]: { [theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
width: '25%', width: '50%',
}, },
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
width: '100%', width: '100%',
@ -26,9 +29,6 @@ const useStyles = makeStyles((theme) => ({
server: { server: {
width: '35%', width: '35%',
height: '100%', height: '100%',
[theme.breakpoints.down('lg')]: {
width: '25%',
},
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
width: '100%', width: '100%',
}, },
@ -46,6 +46,7 @@ const useStyles = makeStyles((theme) => ({
const Home = (): JSX.Element => { const Home = (): JSX.Element => {
const classes = useStyles(); const classes = useStyles();
const width = useWindowDimensions();
const { data } = PeriodicFetch<AnvilList>( const { data } = PeriodicFetch<AnvilList>(
`${process.env.NEXT_PUBLIC_API_URL}/get_anvils`, `${process.env.NEXT_PUBLIC_API_URL}/get_anvils`,
@ -53,9 +54,14 @@ const Home = (): JSX.Element => {
return ( return (
<> <>
<Head>
<title>Dashboard</title>
</Head>
<AnvilProvider> <AnvilProvider>
<Header /> <Header />
{data?.anvils && ( {data?.anvils &&
width &&
(width > LARGE_MOBILE_BREAKPOINT ? (
<Box className={classes.container}> <Box className={classes.container}>
<Box className={classes.child}> <Box className={classes.child}>
<Anvils list={data} /> <Anvils list={data} />
@ -73,7 +79,21 @@ const Home = (): JSX.Element => {
<Memory /> <Memory />
</Box> </Box>
</Box> </Box>
)} ) : (
<Box className={classes.container}>
<Box className={classes.child}>
<Servers anvil={data.anvils} />
<Anvils list={data} />
<Hosts anvil={data.anvils} />
</Box>
<Box className={classes.child}>
<Network />
<SharedStorage />
<CPU />
<Memory />
</Box>
</Box>
))}
</AnvilProvider> </AnvilProvider>
</> </>
); );

@ -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;

@ -1,34 +1,34 @@
declare type AnvilConnection = { declare type AnvilConnection = {
protocol: 'async_a' | 'sync_c'; protocol: 'async_a' | 'sync_c';
connection_state: string;
fencing: string;
targets: Array<{ targets: Array<{
target_name: string; target_name: string;
states: { target_host_uuid: string;
connection: string; disk_state: string;
disk: string;
};
role: string; role: string;
logical_volume_path: string; logical_volume_path?: string;
}>; }>;
resync?: { resync?: {
rate: number; rate: number;
percent_complete: number; percent_complete: number;
time_remain: number; time_remain: number;
oos_size: number;
}; };
}; };
declare type AnvilVolume = { declare type AnvilVolume = {
index: number; number: number;
drbd_device_path: string; drbd_device_path: string;
drbd_device_minor: number; drbd_device_minor: number;
size: number; size: number;
connections: Array<AnvilConnection>; connections: Array<AnvilConnection>;
}; };
declare type AnvilResource = { declare type AnvilReplicatedStorage = {
resource_name: string; resource_name: string;
resource_host_uuid: string;
is_active: boolean;
timestamp: number;
volumes: Array<AnvilVolume>; volumes: Array<AnvilVolume>;
}; };
declare type AnvilReplicatedStorage = {
resources: Array<AnvilResource>;
};

@ -0,0 +1 @@
declare module '@novnc/novnc/core/rfb';
Loading…
Cancel
Save