Merge pull request #175 from Tsu-ba-me/issues/151-vnc-frontend

Web UI: add the front-end counterpart for VNC
main
Digimer 3 years ago committed by GitHub
commit f1af3d4e14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      striker-ui/.eslintrc.json
  2. 1
      striker-ui/.gitignore
  3. 13
      striker-ui/Makefile.am
  4. 20
      striker-ui/components/AnvilDrawer.tsx
  5. 9
      striker-ui/components/Anvils/AnvilList.tsx
  6. 4
      striker-ui/components/Anvils/SelectedAnvil.tsx
  7. 239
      striker-ui/components/Display/FullSize.tsx
  8. 79
      striker-ui/components/Display/Preview.tsx
  9. 96
      striker-ui/components/Display/VncDisplay.tsx
  10. 4
      striker-ui/components/Display/index.tsx
  11. 27
      striker-ui/components/Display/keyCombinations.ts
  12. 12
      striker-ui/components/Domain.tsx
  13. 3
      striker-ui/components/FileSystem/FileSystems.tsx
  14. 127
      striker-ui/components/Header.tsx
  15. 28
      striker-ui/components/Hosts/AnvilHost.tsx
  16. 8
      striker-ui/components/Network/Network.tsx
  17. 2
      striker-ui/components/Panels/InnerPanel.tsx
  18. 13
      striker-ui/components/Panels/Panel.tsx
  19. 131
      striker-ui/components/Resource/ResourceVolumes.tsx
  20. 18
      striker-ui/components/Resource/index.tsx
  21. 11
      striker-ui/components/Servers.tsx
  22. 4
      striker-ui/components/SharedStorage/SharedStorage.tsx
  23. 19
      striker-ui/hooks/useWindowDimenions.ts
  24. 1
      striker-ui/lib/consts/DEFAULT_THEME.ts
  25. 5
      striker-ui/lib/consts/ICONS.ts
  26. 12
      striker-ui/lib/fetchers/putFetch.ts
  27. 25
      striker-ui/lib/fetchers/putFetchWithTimeout.ts
  28. 11
      striker-ui/lib/fetchers/putJSON.ts
  29. 1
      striker-ui/out/_next/static/3u8VlnDkIB4kvVnXXJdo9/_buildManifest.js
  30. 1
      striker-ui/out/_next/static/WegM9eT5kchrLUpg3bXOi/_buildManifest.js
  31. 0
      striker-ui/out/_next/static/WegM9eT5kchrLUpg3bXOi/_ssgManifest.js
  32. 1
      striker-ui/out/_next/static/chunks/204.04ef0f70c11fb4c25e5c.js
  33. 1
      striker-ui/out/_next/static/chunks/254-408dda06d3f8a3c7f26d.js
  34. 1
      striker-ui/out/_next/static/chunks/340.717e8436d6d29df37ce9.js
  35. 1
      striker-ui/out/_next/static/chunks/642-0e4040fe0a744c110cab.js
  36. 1
      striker-ui/out/_next/static/chunks/642-ebd3de567e50b02b8111.js
  37. 1
      striker-ui/out/_next/static/chunks/643-8d1f5368d89a6ae0ce2a.js
  38. 2
      striker-ui/out/_next/static/chunks/framework-c93ed74a065331c4bd75.js
  39. 1
      striker-ui/out/_next/static/chunks/pages/_app-88ba5c92fa3ac1d52da7.js
  40. 1
      striker-ui/out/_next/static/chunks/pages/_app-f07dad954b186d55bf72.js
  41. 1
      striker-ui/out/_next/static/chunks/pages/index-c61930195d75a6c617a7.js
  42. 1
      striker-ui/out/_next/static/chunks/pages/index-ca8a2930d2c5ccf8a7f5.js
  43. 1
      striker-ui/out/_next/static/chunks/pages/server-70802da45b05d679f5bd.js
  44. 1
      striker-ui/out/_next/static/chunks/webpack-189c53927ffd3caf09c3.js
  45. 1
      striker-ui/out/_next/static/chunks/webpack-279cb3a826ca5d40fce3.js
  46. 34
      striker-ui/out/index.html
  47. 516
      striker-ui/out/server.html
  48. 1199
      striker-ui/package-lock.json
  49. 24
      striker-ui/package.json
  50. 64
      striker-ui/pages/index.tsx
  51. 57
      striker-ui/pages/server/index.tsx
  52. 2
      striker-ui/theme/index.ts
  53. 22
      striker-ui/types/AnvilReplicatedStorage.d.ts
  54. 1
      striker-ui/types/novnc__novnc.d.ts

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

@ -15,6 +15,7 @@
/build
/out/*
!/out/index.html
!/out/server.html
!/out/_next
# misc

@ -7,7 +7,8 @@ nextbuilddir = .next
# List of paths relative to the build output directory.
#
outindexfile = index.html
outindexpage = index.html
outserverpage = server.html
outjsmodulesdir = _next
outimagesdir = pngs
@ -88,13 +89,19 @@ build: $(nodemodulesdir)
install-data-hook:
-@echo "Place build output files."
cp -r --no-preserve=mode $(srcdir)/$(nextoutdir)/$(outindexfile) $(srcdir)/$(nextoutdir)/$(outjsmodulesdir) $(DESTDIR)/$(htmldir)/
(cd $(srcdir)/$(nextoutdir); \
cp -r --no-preserve=mode \
$(outindexpage) $(outserverpage) $(outjsmodulesdir) \
$(DESTDIR)/$(htmldir)/ \
)
-@echo "Create symlink to images to enable borrowing icon etc. without duplicating."
(cd $(DESTDIR)/$(htmldir); $(LN_S) skins/alteeve/images $(outimagesdir))
uninstall-hook:
-@echo "Remove all installed files of the current module."
(cd $(DESTDIR)/$(htmldir); rm -rf $(outindexfile) $(outjsmodulesdir) $(outimagesdir))
(cd $(DESTDIR)/$(htmldir); \
rm -rf $(outindexpage) $(outserverpage) $(outjsmodulesdir) $(outimagesdir) \
)
clean-local:
-@echo "Clean up node modules."

@ -1,9 +1,10 @@
import { Divider, Drawer, List, ListItem, Box } from '@material-ui/core';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import DashboardIcon from '@material-ui/icons/Dashboard';
import { Dispatch, SetStateAction } from 'react';
import { BodyText, HeaderText } from './Text';
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 {
open: boolean;
@ -22,6 +23,13 @@ const useStyles = makeStyles(() =>
paddingTop: '.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" />
</ListItem>
<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(
(icon): JSX.Element => (
<ListItem

@ -1,7 +1,11 @@
import { useContext, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
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 { AnvilContext } from '../AnvilContext';
import sortAnvils from './sortAnvils';
@ -12,7 +16,8 @@ const useStyles = makeStyles((theme) => ({
width: '100%',
overflow: 'auto',
height: '30vh',
[theme.breakpoints.down('md')]: {
paddingRight: '.3em',
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%',
overflow: 'hidden',
},

@ -6,7 +6,7 @@ import { SELECTED_ANVIL } from '../../lib/consts/DEFAULT_THEME';
import anvilState from '../../lib/consts/ANVILS';
import { AnvilContext } from '../AnvilContext';
import Decorator, { Colours } from '../Decorator';
import putJSON from '../../lib/fetchers/putJSON';
import putFetch from '../../lib/fetchers/putFetch';
const useStyles = makeStyles(() => ({
root: {
@ -67,7 +67,7 @@ const SelectedAnvil = ({ list }: { list: AnvilListItem[] }): JSX.Element => {
<Switch
checked={isAnvilOn(list[index])}
onChange={() =>
putJSON('/set_power', {
putFetch(`${process.env.NEXT_PUBLIC_API_URL}/set_power`, {
anvil_uuid: list[index].anvil_uuid,
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 { AnvilContext } from '../AnvilContext';
import Spinner from '../Spinner';
import { LARGE_MOBILE_BREAKPOINT } from '../../lib/consts/DEFAULT_THEME';
const useStyles = makeStyles((theme) => ({
header: {
@ -18,7 +19,7 @@ const useStyles = makeStyles((theme) => ({
overflow: 'auto',
height: '78vh',
paddingLeft: '.3em',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down(LARGE_MOBILE_BREAKPOINT)]: {
height: '100%',
},
},

@ -1,43 +1,45 @@
import { useState } from 'react';
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 { ICONS, ICON_SIZE } from '../lib/consts/ICONS';
import { BORDER_RADIUS, RED } from '../lib/consts/DEFAULT_THEME';
import AnvilDrawer from './AnvilDrawer';
const useStyles = makeStyles((theme) =>
createStyles({
appBar: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
borderBottom: 'solid 1px',
borderBottomColor: RED,
const useStyles = makeStyles((theme) => ({
appBar: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
borderBottom: 'solid 1px',
borderBottomColor: RED,
},
input: {
height: '2.8em',
width: '30vw',
backgroundColor: theme.palette.secondary.main,
borderRadius: BORDER_RADIUS,
},
barElement: {
padding: 0,
},
iconBox: {
[theme.breakpoints.down('sm')]: {
display: 'none',
},
input: {
height: '2.8em',
width: '30vw',
backgroundColor: theme.palette.secondary.main,
borderRadius: BORDER_RADIUS,
},
searchBar: {
[theme.breakpoints.down('sm')]: {
flexGrow: 1,
paddingLeft: '15vw',
},
barElement: {
padding: 0,
},
icons: {
[theme.breakpoints.down('sm')]: {
display: 'none',
},
},
searchBar: {
[theme.breakpoints.down('sm')]: {
flexGrow: 1,
paddingLeft: '15vw',
},
},
}),
);
},
icons: {
paddingLeft: '.1em',
paddingRight: '.1em',
},
}));
const Header = (): JSX.Element => {
const classes = useStyles();
@ -46,40 +48,39 @@ const Header = (): JSX.Element => {
const toggleDrawer = (): void => setOpen(!open);
return (
<>
<AppBar position="static" className={classes.appBar}>
<Box display="flex" justifyContent="space-between" flexDirection="row">
<Box className={classes.barElement}>
<Button onClick={toggleDrawer}>
<img alt="" src="/pngs/logo.png" width="160" height="40" />
</Button>
</Box>
<Box className={`${classes.barElement} ${classes.icons}`}>
{ICONS.map(
(icon): JSX.Element => (
<a
key={icon.uri}
href={
icon.uri.search(/^https?:/) !== -1
? icon.uri
: `${process.env.NEXT_PUBLIC_API_URL}${icon.uri}`
}
>
<img
alt=""
key="icon"
src={icon.image}
// eslint-disable-next-line react/jsx-props-no-spreading
{...ICON_SIZE}
/>
</a>
),
)}
</Box>
<AppBar position="static" className={classes.appBar}>
<Box display="flex" justifyContent="space-between" flexDirection="row">
<Box className={classes.barElement}>
<Button onClick={toggleDrawer}>
<img alt="" src="/pngs/logo.png" width="160" height="40" />
</Button>
</Box>
<Box className={`${classes.barElement} ${classes.iconBox}`}>
{ICONS.map(
(icon): JSX.Element => (
<a
key={icon.uri}
href={
icon.uri.search(/^https?:/) !== -1
? icon.uri
: `${process.env.NEXT_PUBLIC_API_URL}${icon.uri}`
}
>
<img
alt=""
key="icon"
src={icon.image}
// eslint-disable-next-line react/jsx-props-no-spreading
{...ICON_SIZE}
className={classes.icons}
/>
</a>
),
)}
</Box>
</AppBar>
</Box>
<AnvilDrawer open={open} setOpen={setOpen} />
</>
</AppBar>
);
};

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

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

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

@ -42,6 +42,19 @@ const useStyles = makeStyles(() => ({
bottom: '-.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 => {

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

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

@ -29,6 +29,11 @@ export const ICONS = [
image: '/pngs/email_on.png',
uri: '/striker?email=true',
},
{
text: 'Logout',
image: '/pngs/users_icon_on.png',
uri: '/striker?logout=true',
},
{
text: 'Help',
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;

@ -1 +0,0 @@
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/642-0e4040fe0a744c110cab.js","static/chunks/pages/index-ca8a2930d2c5ccf8a7f5.js"],"/_error":["static/chunks/pages/_error-a9f53acb468cbab8a6cb.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

@ -0,0 +1 @@
self.__BUILD_MANIFEST=function(e){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[e,"static/chunks/254-408dda06d3f8a3c7f26d.js","static/chunks/pages/index-c61930195d75a6c617a7.js"],"/_error":["static/chunks/pages/_error-a9f53acb468cbab8a6cb.js"],"/server":[e,"static/chunks/643-8d1f5368d89a6ae0ce2a.js","static/chunks/pages/server-70802da45b05d679f5bd.js"],sortedPages:["/","/_app","/_error","/server"]}}("static/chunks/642-ebd3de567e50b02b8111.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[340],{340:function(e,r,n){"use strict";n.r(r);var t=n(5893),c=n(7294),u=n(8753),o=function(e){var r=(0,c.useRef)(null),n=e.rfb,o=e.url,i=e.viewOnly,s=e.focusOnClick,l=e.clipViewport,f=e.dragViewport,v=e.scaleViewport,a=e.resizeSession,d=e.showDotCursor,w=e.background,p=e.qualityLevel,m=e.compressionLevel;(0,c.useEffect)((function(){return r.current?(n.current||(r.current.innerHTML="",n.current=new u.Z(r.current,o),n.current.viewOnly=i,n.current.focusOnClick=s,n.current.clipViewport=l,n.current.dragViewport=f,n.current.resizeSession=a,n.current.scaleViewport=v,n.current.showDotCursor=d,n.current.background=w,n.current.qualityLevel=p,n.current.compressionLevel=m),n.current?function(){n.current&&(n.current.disconnect(),n.current=void 0)}:void 0):function(){n.current&&(null===n||void 0===n||n.current.disconnect(),n.current=void 0)}}),[n]);return(0,t.jsx)("div",{style:{width:"100%",height:"75vh"},ref:r,onMouseEnter:function(){document.activeElement&&document.activeElement instanceof HTMLElement&&document.activeElement.blur(),null!==n&&void 0!==n&&n.current&&n.current.focus()}})};r.default=(0,c.memo)(o)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
!function(){"use strict";var r={},e={};function t(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}},u=!0;try{r[n](i,i.exports,t),u=!1}finally{u&&delete e[n]}return i.exports}t.m=r,function(){var r=[];t.O=function(e,n,o,i){if(!n){var u=1/0;for(a=0;a<r.length;a++){n=r[a][0],o=r[a][1],i=r[a][2];for(var c=!0,f=0;f<n.length;f++)(!1&i||u>=i)&&Object.keys(t.O).every((function(r){return t.O[r](n[f])}))?n.splice(f--,1):(c=!1,i<u&&(u=i));c&&(r.splice(a--,1),e=o())}return e}i=i||0;for(var a=r.length;a>0&&r[a-1][2]>i;a--)r[a]=r[a-1];r[a]=[n,o,i]}}(),t.n=function(r){var e=r&&r.__esModule?function(){return r.default}:function(){return r};return t.d(e,{a:e}),e},t.d=function(r,e){for(var n in e)t.o(e,n)&&!t.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:e[n]})},t.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(r){if("object"===typeof window)return window}}(),t.o=function(r,e){return Object.prototype.hasOwnProperty.call(r,e)},t.r=function(r){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(r,"__esModule",{value:!0})},function(){var r;t.g.importScripts&&(r=t.g.location+"");var e=t.g.document;if(!r&&e&&(e.currentScript&&(r=e.currentScript.src),!r)){var n=e.getElementsByTagName("script");n.length&&(r=n[n.length-1].src)}if(!r)throw new Error("Automatic publicPath is not supported in this browser");r=r.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),t.p=r+"../../"}(),function(){var r={272:0};t.O.j=function(e){return 0===r[e]};var e=function(e,n){var o,i,u=n[0],c=n[1],f=n[2],a=0;for(o in c)t.o(c,o)&&(t.m[o]=c[o]);if(f)var l=f(t);for(e&&e(n);a<u.length;a++)i=u[a],t.o(r,i)&&r[i]&&r[i][0](),r[u[a]]=0;return t.O(l)},n=self.webpackChunk_N_E=self.webpackChunk_N_E||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))}()}();

@ -0,0 +1 @@
!function(){"use strict";var e={},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}},u=!0;try{e[n](i,i.exports,r),u=!1}finally{u&&delete t[n]}return i.exports}r.m=e,function(){var e=[];r.O=function(t,n,o,i){if(!n){var u=1/0;for(f=0;f<e.length;f++){n=e[f][0],o=e[f][1],i=e[f][2];for(var c=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(r.O).every((function(e){return r.O[e](n[a])}))?n.splice(a--,1):(c=!1,i<u&&(u=i));c&&(e.splice(f--,1),t=o())}return t}i=i||0;for(var f=e.length;f>0&&e[f-1][2]>i;f--)e[f]=e[f-1];e[f]=[n,o,i]}}(),r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.f={},r.e=function(e){return Promise.all(Object.keys(r.f).reduce((function(t,n){return r.f[n](e,t),t}),[]))},r.u=function(e){return"static/chunks/"+e+"."+{204:"04ef0f70c11fb4c25e5c",340:"717e8436d6d29df37ce9"}[e]+".js"},r.miniCssF=function(e){return"static/css/1b1a1a5807b24bb728c2.css"},r.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}(),r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e={},t="_N_E:";r.l=function(n,o,i,u){if(e[n])e[n].push(o);else{var c,a;if(void 0!==i)for(var f=document.getElementsByTagName("script"),l=0;l<f.length;l++){var s=f[l];if(s.getAttribute("src")==n||s.getAttribute("data-webpack")==t+i){c=s;break}}c||(a=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,r.nc&&c.setAttribute("nonce",r.nc),c.setAttribute("data-webpack",t+i),c.src=n),e[n]=[o];var d=function(t,r){c.onerror=c.onload=null,clearTimeout(p);var o=e[n];if(delete e[n],c.parentNode&&c.parentNode.removeChild(c),o&&o.forEach((function(e){return e(r)})),t)return t(r)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=d.bind(null,c.onerror),c.onload=d.bind(null,c.onload),a&&document.head.appendChild(c)}}}(),r.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e;r.g.importScripts&&(e=r.g.location+"");var t=r.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var n=t.getElementsByTagName("script");n.length&&(e=n[n.length-1].src)}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),r.p=e+"../../"}(),function(){var e={272:0};r.f.j=function(t,n){var o=r.o(e,t)?e[t]:void 0;if(0!==o)if(o)n.push(o[2]);else if(272!=t){var i=new Promise((function(r,n){o=e[t]=[r,n]}));n.push(o[2]=i);var u=r.p+r.u(t),c=new Error;r.l(u,(function(n){if(r.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var i=n&&("load"===n.type?"missing":n.type),u=n&&n.target&&n.target.src;c.message="Loading chunk "+t+" failed.\n("+i+": "+u+")",c.name="ChunkLoadError",c.type=i,c.request=u,o[1](c)}}),"chunk-"+t,t)}else e[t]=0},r.O.j=function(t){return 0===e[t]};var t=function(t,n){var o,i,u=n[0],c=n[1],a=n[2],f=0;for(o in c)r.o(c,o)&&(r.m[o]=c[o]);if(a)var l=a(r);for(t&&t(n);f<u.length;f++)i=u[f],r.o(e,i)&&e[i]&&e[i][0](),e[u[f]]=0;return r.O(l)},n=self.webpackChunk_N_E=self.webpackChunk_N_E||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))}()}();

@ -1,4 +1,4 @@
<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="/_next/static/css/1b1a1a5807b24bb728c2.css" as="style"/><link rel="stylesheet" href="/_next/static/css/1b1a1a5807b24bb728c2.css" data-n-g=""/><noscript data-n-css=""></noscript><link rel="preload" href="/_next/static/chunks/webpack-189c53927ffd3caf09c3.js" as="script"/><link rel="preload" href="/_next/static/chunks/framework-2191d16384373197bc0a.js" as="script"/><link rel="preload" href="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/_app-88ba5c92fa3ac1d52da7.js" as="script"/><link rel="preload" href="/_next/static/chunks/642-0e4040fe0a744c110cab.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/index-ca8a2930d2c5ccf8a7f5.js" as="script"/><style id="jss-server-side">.MuiButtonBase-root {
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width"/><meta charSet="utf-8"/><title>Dashboard</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/1b1a1a5807b24bb728c2.css" as="style"/><link rel="stylesheet" href="/_next/static/css/1b1a1a5807b24bb728c2.css" data-n-g=""/><noscript data-n-css=""></noscript><link rel="preload" href="/_next/static/chunks/webpack-279cb3a826ca5d40fce3.js" as="script"/><link rel="preload" href="/_next/static/chunks/framework-c93ed74a065331c4bd75.js" as="script"/><link rel="preload" href="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/_app-f07dad954b186d55bf72.js" as="script"/><link rel="preload" href="/_next/static/chunks/642-ebd3de567e50b02b8111.js" as="script"/><link rel="preload" href="/_next/static/chunks/254-408dda06d3f8a3c7f26d.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/index-c61930195d75a6c617a7.js" as="script"/><style id="jss-server-side">.MuiButtonBase-root {
color: inherit;
border: 0;
cursor: pointer;
@ -31,13 +31,13 @@
}
}
.jss9 {
.jss10 {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.jss10 { }
.jss11 { }
.jss12 { }
.MuiButton-root {
color: rgba(0, 0, 0, 0.87);
padding: 6px 16px;
@ -450,16 +450,23 @@
color: inherit;
background-color: transparent;
}
.jss12 {
.jss13 {
width: 200px;
}
.jss13 {
.jss14 {
background: #888;
}
.jss14 {
.jss15 {
padding-top: .5em;
padding-left: 1.5em;
}
.jss16 {
padding-left: .1em;
}
.jss17 {
color: #E5E5E5;
font-size: 2.3em;
}
.jss4 {
padding-top: 4px;
padding-left: 24px;
@ -488,13 +495,17 @@
padding-left: 15vw;
}
}
.jss9 {
padding-left: .1em;
padding-right: .1em;
}
.jss1 {
width: 22%;
height: 100%;
}
@media (max-width:1919.95px) {
@media (max-width:1799.95px) {
.jss1 {
width: 25%;
width: 50%;
}
}
@media (max-width:1279.95px) {
@ -506,11 +517,6 @@
width: 35%;
height: 100%;
}
@media (max-width:1919.95px) {
.jss2 {
width: 25%;
}
}
@media (max-width:1279.95px) {
.jss2 {
width: 100%;
@ -526,4 +532,4 @@
.jss3 {
display: block;
}
}</style></head><body><div id="__next"><header class="MuiPaper-root MuiAppBar-root MuiAppBar-positionStatic MuiAppBar-colorPrimary jss4 MuiPaper-elevation4"><div class="MuiBox-root jss9"><div class="MuiBox-root jss10 jss6"><button class="MuiButtonBase-root MuiButton-root MuiButton-text" tabindex="0" type="button"><span class="MuiButton-label"><img alt="" src="/pngs/logo.png" width="160" height="40"/></span></button></div><div class="MuiBox-root jss11 jss6 jss7"><a href="/cgi-bin/striker?files=true"><img alt="" src="/pngs/files_on.png" width="40em" height="40em"/></a><a href="/cgi-bin/striker?jobs=true"><img alt="" src="/pngs/tasks_no-jobs_icon.png" width="40em" height="40em"/></a><a href="/cgi-bin/striker?configure=true"><img alt="" src="/pngs/configure_icon_on.png" width="40em" height="40em"/></a><a href="/cgi-bin/striker?striker=true"><img alt="" src="/pngs/striker_icon_on.png" width="40em" height="40em"/></a><a href="/cgi-bin/striker?anvil=true"><img alt="" src="/pngs/anvil_icon_on.png" width="40em" height="40em"/></a><a href="/cgi-bin/striker?email=true"><img alt="" src="/pngs/email_on.png" width="40em" height="40em"/></a><a href="https://alteeve.com/w/Support"><img alt="" src="/pngs/help_icon_on.png" width="40em" height="40em"/></a></div></div></header></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"3u8VlnDkIB4kvVnXXJdo9","nextExport":true,"autoExport":true,"isFallback":false}</script><script nomodule="" src="/_next/static/chunks/polyfills-eef578260fd80f8fff94.js"></script><script src="/_next/static/chunks/webpack-189c53927ffd3caf09c3.js" async=""></script><script src="/_next/static/chunks/framework-2191d16384373197bc0a.js" async=""></script><script src="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" async=""></script><script src="/_next/static/chunks/pages/_app-88ba5c92fa3ac1d52da7.js" async=""></script><script src="/_next/static/chunks/642-0e4040fe0a744c110cab.js" async=""></script><script src="/_next/static/chunks/pages/index-ca8a2930d2c5ccf8a7f5.js" async=""></script><script src="/_next/static/3u8VlnDkIB4kvVnXXJdo9/_buildManifest.js" async=""></script><script src="/_next/static/3u8VlnDkIB4kvVnXXJdo9/_ssgManifest.js" async=""></script></body></html>
}</style></head><body><div id="__next"><header class="MuiPaper-root MuiAppBar-root MuiAppBar-positionStatic MuiAppBar-colorPrimary jss4 MuiPaper-elevation4"><div class="MuiBox-root jss10"><div class="MuiBox-root jss11 jss6"><button class="MuiButtonBase-root MuiButton-root MuiButton-text" tabindex="0" type="button"><span class="MuiButton-label"><img alt="" src="/pngs/logo.png" width="160" height="40"/></span></button></div><div class="MuiBox-root jss12 jss6 jss7"><a href="/cgi-bin/striker?files=true"><img alt="" src="/pngs/files_on.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?jobs=true"><img alt="" src="/pngs/tasks_no-jobs_icon.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?configure=true"><img alt="" src="/pngs/configure_icon_on.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?striker=true"><img alt="" src="/pngs/striker_icon_on.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?anvil=true"><img alt="" src="/pngs/anvil_icon_on.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?email=true"><img alt="" src="/pngs/email_on.png" width="40em" height="40em" class="jss9"/></a><a href="/cgi-bin/striker?logout=true"><img alt="" src="/pngs/users_icon_on.png" width="40em" height="40em" class="jss9"/></a><a href="https://alteeve.com/w/Support"><img alt="" src="/pngs/help_icon_on.png" width="40em" height="40em" class="jss9"/></a></div></div></header></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"WegM9eT5kchrLUpg3bXOi","nextExport":true,"autoExport":true,"isFallback":false}</script><script nomodule="" src="/_next/static/chunks/polyfills-eef578260fd80f8fff94.js"></script><script src="/_next/static/chunks/webpack-279cb3a826ca5d40fce3.js" async=""></script><script src="/_next/static/chunks/framework-c93ed74a065331c4bd75.js" async=""></script><script src="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" async=""></script><script src="/_next/static/chunks/pages/_app-f07dad954b186d55bf72.js" async=""></script><script src="/_next/static/chunks/642-ebd3de567e50b02b8111.js" async=""></script><script src="/_next/static/chunks/254-408dda06d3f8a3c7f26d.js" async=""></script><script src="/_next/static/chunks/pages/index-c61930195d75a6c617a7.js" async=""></script><script src="/_next/static/WegM9eT5kchrLUpg3bXOi/_buildManifest.js" async=""></script><script src="/_next/static/WegM9eT5kchrLUpg3bXOi/_ssgManifest.js" async=""></script></body></html>

@ -0,0 +1,516 @@
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width"/><meta charSet="utf-8"/><title></title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/1b1a1a5807b24bb728c2.css" as="style"/><link rel="stylesheet" href="/_next/static/css/1b1a1a5807b24bb728c2.css" data-n-g=""/><noscript data-n-css=""></noscript><link rel="preload" href="/_next/static/chunks/webpack-279cb3a826ca5d40fce3.js" as="script"/><link rel="preload" href="/_next/static/chunks/framework-c93ed74a065331c4bd75.js" as="script"/><link rel="preload" href="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/_app-f07dad954b186d55bf72.js" as="script"/><link rel="preload" href="/_next/static/chunks/642-ebd3de567e50b02b8111.js" as="script"/><link rel="preload" href="/_next/static/chunks/643-8d1f5368d89a6ae0ce2a.js" as="script"/><link rel="preload" href="/_next/static/chunks/pages/server-70802da45b05d679f5bd.js" as="script"/><style id="jss-server-side">.MuiButtonBase-root {
color: inherit;
border: 0;
cursor: pointer;
margin: 0;
display: inline-flex;
outline: 0;
padding: 0;
position: relative;
align-items: center;
user-select: none;
border-radius: 0;
vertical-align: middle;
-moz-appearance: none;
justify-content: center;
text-decoration: none;
background-color: transparent;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
}
.MuiButtonBase-root::-moz-focus-inner {
border-style: none;
}
.MuiButtonBase-root.Mui-disabled {
cursor: default;
pointer-events: none;
}
@media print {
.MuiButtonBase-root {
color-adjust: exact;
}
}
.jss9 {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.jss10 { }
.jss11 { }
.MuiButton-root {
color: rgba(0, 0, 0, 0.87);
padding: 6px 16px;
font-size: 0.875rem;
min-width: 64px;
box-sizing: border-box;
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
font-family: Roboto Condensed;
font-weight: 500;
line-height: 1.75;
border-radius: 4px;
text-transform: uppercase;
}
.MuiButton-root:hover {
text-decoration: none;
background-color: rgba(0, 0, 0, 0.04);
}
.MuiButton-root.Mui-disabled {
color: rgba(0, 0, 0, 0.26);
}
@media (hover: none) {
.MuiButton-root:hover {
background-color: transparent;
}
}
.MuiButton-root:hover.Mui-disabled {
background-color: transparent;
}
.MuiButton-label {
width: 100%;
display: inherit;
align-items: inherit;
justify-content: inherit;
}
.MuiButton-text {
padding: 6px 8px;
}
.MuiButton-textPrimary {
color: #343434;
}
.MuiButton-textPrimary:hover {
background-color: rgba(52, 52, 52, 0.04);
}
@media (hover: none) {
.MuiButton-textPrimary:hover {
background-color: transparent;
}
}
.MuiButton-textSecondary {
color: #F2F2F2;
}
.MuiButton-textSecondary:hover {
background-color: rgba(242, 242, 242, 0.04);
}
@media (hover: none) {
.MuiButton-textSecondary:hover {
background-color: transparent;
}
}
.MuiButton-outlined {
border: 1px solid rgba(0, 0, 0, 0.23);
padding: 5px 15px;
}
.MuiButton-outlined.Mui-disabled {
border: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiButton-outlinedPrimary {
color: #343434;
border: 1px solid rgba(52, 52, 52, 0.5);
}
.MuiButton-outlinedPrimary:hover {
border: 1px solid #343434;
background-color: rgba(52, 52, 52, 0.04);
}
@media (hover: none) {
.MuiButton-outlinedPrimary:hover {
background-color: transparent;
}
}
.MuiButton-outlinedSecondary {
color: #F2F2F2;
border: 1px solid rgba(242, 242, 242, 0.5);
}
.MuiButton-outlinedSecondary:hover {
border: 1px solid #F2F2F2;
background-color: rgba(242, 242, 242, 0.04);
}
.MuiButton-outlinedSecondary.Mui-disabled {
border: 1px solid rgba(0, 0, 0, 0.26);
}
@media (hover: none) {
.MuiButton-outlinedSecondary:hover {
background-color: transparent;
}
}
.MuiButton-contained {
color: rgba(0, 0, 0, 0.87);
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);
background-color: #e0e0e0;
}
.MuiButton-contained:hover {
box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12);
background-color: #d5d5d5;
}
.MuiButton-contained.Mui-focusVisible {
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12);
}
.MuiButton-contained:active {
box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12);
}
.MuiButton-contained.Mui-disabled {
color: rgba(0, 0, 0, 0.26);
box-shadow: none;
background-color: rgba(0, 0, 0, 0.12);
}
@media (hover: none) {
.MuiButton-contained:hover {
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);
background-color: #e0e0e0;
}
}
.MuiButton-contained:hover.Mui-disabled {
background-color: rgba(0, 0, 0, 0.12);
}
.MuiButton-containedPrimary {
color: #fff;
background-color: #343434;
}
.MuiButton-containedPrimary:hover {
background-color: rgb(36, 36, 36);
}
@media (hover: none) {
.MuiButton-containedPrimary:hover {
background-color: #343434;
}
}
.MuiButton-containedSecondary {
color: rgba(0, 0, 0, 0.87);
background-color: #F2F2F2;
}
.MuiButton-containedSecondary:hover {
background-color: rgb(169, 169, 169);
}
@media (hover: none) {
.MuiButton-containedSecondary:hover {
background-color: #F2F2F2;
}
}
.MuiButton-disableElevation {
box-shadow: none;
}
.MuiButton-disableElevation:hover {
box-shadow: none;
}
.MuiButton-disableElevation.Mui-focusVisible {
box-shadow: none;
}
.MuiButton-disableElevation:active {
box-shadow: none;
}
.MuiButton-disableElevation.Mui-disabled {
box-shadow: none;
}
.MuiButton-colorInherit {
color: inherit;
border-color: currentColor;
}
.MuiButton-textSizeSmall {
padding: 4px 5px;
font-size: 0.8125rem;
}
.MuiButton-textSizeLarge {
padding: 8px 11px;
font-size: 0.9375rem;
}
.MuiButton-outlinedSizeSmall {
padding: 3px 9px;
font-size: 0.8125rem;
}
.MuiButton-outlinedSizeLarge {
padding: 7px 21px;
font-size: 0.9375rem;
}
.MuiButton-containedSizeSmall {
padding: 4px 10px;
font-size: 0.8125rem;
}
.MuiButton-containedSizeLarge {
padding: 8px 22px;
font-size: 0.9375rem;
}
.MuiButton-fullWidth {
width: 100%;
}
.MuiButton-startIcon {
display: inherit;
margin-left: -4px;
margin-right: 8px;
}
.MuiButton-startIcon.MuiButton-iconSizeSmall {
margin-left: -2px;
}
.MuiButton-endIcon {
display: inherit;
margin-left: 8px;
margin-right: -4px;
}
.MuiButton-endIcon.MuiButton-iconSizeSmall {
margin-right: -2px;
}
.MuiButton-iconSizeSmall > *:first-child {
font-size: 18px;
}
.MuiButton-iconSizeMedium > *:first-child {
font-size: 20px;
}
.MuiButton-iconSizeLarge > *:first-child {
font-size: 22px;
}
.MuiDrawer-docked {
flex: 0 0 auto;
}
.MuiDrawer-paper {
top: 0;
flex: 1 0 auto;
height: 100%;
display: flex;
outline: 0;
z-index: 1200;
position: fixed;
overflow-y: auto;
flex-direction: column;
-webkit-overflow-scrolling: touch;
}
.MuiDrawer-paperAnchorLeft {
left: 0;
right: auto;
}
.MuiDrawer-paperAnchorRight {
left: auto;
right: 0;
}
.MuiDrawer-paperAnchorTop {
top: 0;
left: 0;
right: 0;
bottom: auto;
height: auto;
max-height: 100%;
}
.MuiDrawer-paperAnchorBottom {
top: auto;
left: 0;
right: 0;
bottom: 0;
height: auto;
max-height: 100%;
}
.MuiDrawer-paperAnchorDockedLeft {
border-right: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiDrawer-paperAnchorDockedTop {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiDrawer-paperAnchorDockedRight {
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiDrawer-paperAnchorDockedBottom {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiPaper-root {
color: rgba(0, 0, 0, 0.87);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
background-color: #343434;
}
.MuiPaper-rounded {
border-radius: 4px;
}
.MuiPaper-outlined {
border: 1px solid rgba(0, 0, 0, 0.12);
}
.MuiPaper-elevation0 {
box-shadow: none;
}
.MuiPaper-elevation1 {
box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation2 {
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation3 {
box-shadow: 0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.14),0px 1px 8px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation4 {
box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation5 {
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 5px 8px 0px rgba(0,0,0,0.14),0px 1px 14px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation6 {
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12);
}
.MuiPaper-elevation7 {
box-shadow: 0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.14),0px 2px 16px 1px rgba(0,0,0,0.12);
}
.MuiPaper-elevation8 {
box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12);
}
.MuiPaper-elevation9 {
box-shadow: 0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.14),0px 3px 16px 2px rgba(0,0,0,0.12);
}
.MuiPaper-elevation10 {
box-shadow: 0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.14),0px 4px 18px 3px rgba(0,0,0,0.12);
}
.MuiPaper-elevation11 {
box-shadow: 0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.14),0px 4px 20px 3px rgba(0,0,0,0.12);
}
.MuiPaper-elevation12 {
box-shadow: 0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.14),0px 5px 22px 4px rgba(0,0,0,0.12);
}
.MuiPaper-elevation13 {
box-shadow: 0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.14),0px 5px 24px 4px rgba(0,0,0,0.12);
}
.MuiPaper-elevation14 {
box-shadow: 0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.14),0px 5px 26px 4px rgba(0,0,0,0.12);
}
.MuiPaper-elevation15 {
box-shadow: 0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.14),0px 6px 28px 5px rgba(0,0,0,0.12);
}
.MuiPaper-elevation16 {
box-shadow: 0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.14),0px 6px 30px 5px rgba(0,0,0,0.12);
}
.MuiPaper-elevation17 {
box-shadow: 0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.14),0px 6px 32px 5px rgba(0,0,0,0.12);
}
.MuiPaper-elevation18 {
box-shadow: 0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.14),0px 7px 34px 6px rgba(0,0,0,0.12);
}
.MuiPaper-elevation19 {
box-shadow: 0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.14),0px 7px 36px 6px rgba(0,0,0,0.12);
}
.MuiPaper-elevation20 {
box-shadow: 0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.14),0px 8px 38px 7px rgba(0,0,0,0.12);
}
.MuiPaper-elevation21 {
box-shadow: 0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.14),0px 8px 40px 7px rgba(0,0,0,0.12);
}
.MuiPaper-elevation22 {
box-shadow: 0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.14),0px 8px 42px 7px rgba(0,0,0,0.12);
}
.MuiPaper-elevation23 {
box-shadow: 0px 11px 14px -7px rgba(0,0,0,0.2),0px 23px 36px 3px rgba(0,0,0,0.14),0px 9px 44px 8px rgba(0,0,0,0.12);
}
.MuiPaper-elevation24 {
box-shadow: 0px 11px 15px -7px rgba(0,0,0,0.2),0px 24px 38px 3px rgba(0,0,0,0.14),0px 9px 46px 8px rgba(0,0,0,0.12);
}
.MuiAppBar-root {
width: 100%;
display: flex;
z-index: 1100;
box-sizing: border-box;
flex-shrink: 0;
flex-direction: column;
}
.MuiAppBar-positionFixed {
top: 0;
left: auto;
right: 0;
position: fixed;
}
@media print {
.MuiAppBar-positionFixed {
position: absolute;
}
}
.MuiAppBar-positionAbsolute {
top: 0;
left: auto;
right: 0;
position: absolute;
}
.MuiAppBar-positionSticky {
top: 0;
left: auto;
right: 0;
position: sticky;
}
.MuiAppBar-positionStatic {
position: static;
}
.MuiAppBar-positionRelative {
position: relative;
}
.MuiAppBar-colorDefault {
color: rgba(0, 0, 0, 0.87);
background-color: #f5f5f5;
}
.MuiAppBar-colorPrimary {
color: #fff;
background-color: #343434;
}
.MuiAppBar-colorSecondary {
color: rgba(0, 0, 0, 0.87);
background-color: #F2F2F2;
}
.MuiAppBar-colorInherit {
color: inherit;
}
.MuiAppBar-colorTransparent {
color: inherit;
background-color: transparent;
}
.jss12 {
width: 200px;
}
.jss13 {
background: #888;
}
.jss14 {
padding-top: .5em;
padding-left: 1.5em;
}
.jss15 {
padding-left: .1em;
}
.jss16 {
color: #E5E5E5;
font-size: 2.3em;
}
.jss3 {
padding-top: 4px;
padding-left: 24px;
border-bottom: solid 1px;
padding-right: 24px;
padding-bottom: 4px;
border-bottom-color: #D02724;
}
.jss4 {
width: 30vw;
height: 2.8em;
border-radius: 3px;
background-color: #F2F2F2;
}
.jss5 {
padding: 0;
}
@media (max-width:959.95px) {
.jss6 {
display: none;
}
}
@media (max-width:959.95px) {
.jss7 {
flex-grow: 1;
padding-left: 15vw;
}
}
.jss8 {
padding-left: .1em;
padding-right: .1em;
}
.jss1 {
width: 25%;
height: 100%;
}
@media (max-width:1279.95px) {
.jss1 {
width: 100%;
}
}
.jss2 {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
}</style></head><body><div id="__next"><header class="MuiPaper-root MuiAppBar-root MuiAppBar-positionStatic MuiAppBar-colorPrimary jss3 MuiPaper-elevation4"><div class="MuiBox-root jss9"><div class="MuiBox-root jss10 jss5"><button class="MuiButtonBase-root MuiButton-root MuiButton-text" tabindex="0" type="button"><span class="MuiButton-label"><img alt="" src="/pngs/logo.png" width="160" height="40"/></span></button></div><div class="MuiBox-root jss11 jss5 jss6"><a href="/cgi-bin/striker?files=true"><img alt="" src="/pngs/files_on.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?jobs=true"><img alt="" src="/pngs/tasks_no-jobs_icon.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?configure=true"><img alt="" src="/pngs/configure_icon_on.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?striker=true"><img alt="" src="/pngs/striker_icon_on.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?anvil=true"><img alt="" src="/pngs/anvil_icon_on.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?email=true"><img alt="" src="/pngs/email_on.png" width="40em" height="40em" class="jss8"/></a><a href="/cgi-bin/striker?logout=true"><img alt="" src="/pngs/users_icon_on.png" width="40em" height="40em" class="jss8"/></a><a href="https://alteeve.com/w/Support"><img alt="" src="/pngs/help_icon_on.png" width="40em" height="40em" class="jss8"/></a></div></div></header></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/server","query":{},"buildId":"WegM9eT5kchrLUpg3bXOi","nextExport":true,"autoExport":true,"isFallback":false}</script><script nomodule="" src="/_next/static/chunks/polyfills-eef578260fd80f8fff94.js"></script><script src="/_next/static/chunks/webpack-279cb3a826ca5d40fce3.js" async=""></script><script src="/_next/static/chunks/framework-c93ed74a065331c4bd75.js" async=""></script><script src="/_next/static/chunks/main-6c0a2257b76a50556a7f.js" async=""></script><script src="/_next/static/chunks/pages/_app-f07dad954b186d55bf72.js" async=""></script><script src="/_next/static/chunks/642-ebd3de567e50b02b8111.js" async=""></script><script src="/_next/static/chunks/643-8d1f5368d89a6ae0ce2a.js" async=""></script><script src="/_next/static/chunks/pages/server-70802da45b05d679f5bd.js" async=""></script><script src="/_next/static/WegM9eT5kchrLUpg3bXOi/_buildManifest.js" async=""></script><script src="/_next/static/WegM9eT5kchrLUpg3bXOi/_ssgManifest.js" async=""></script></body></html>

File diff suppressed because it is too large Load Diff

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

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

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

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