added config
Some checks failed
Build and Push Docker Image / Build image (push) Has been cancelled

This commit is contained in:
ACoolName 2025-04-09 02:39:33 +03:00
parent 392bfcccaa
commit e81b7abd1a
7 changed files with 767 additions and 723 deletions

View File

@ -1,4 +1,4 @@
FROM node:alpine FROM node:alpine as builder
EXPOSE 3000 EXPOSE 3000
WORKDIR /app WORKDIR /app
@ -9,9 +9,13 @@ COPY tsconfig.json .
RUN npm install RUN npm install
COPY --chmod=111 startup.sh .
COPY public public COPY public public
COPY src src COPY src src
ENTRYPOINT [ "/usr/bin/env", "./startup.sh" ] RUN npm run build
FROM node:alpine
COPY --from=builder /app/build /opt/server
WORKDIR /opt/server
ENTRYPOINT [ "npx", "-y" , "serve", "-s", "/opt/server" ]

1
config.js Normal file
View File

@ -0,0 +1 @@
var API_URL = 'https://games.acooldomain.co'

803
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
public/config.js Normal file
View File

@ -0,0 +1 @@
var API_URL = "http://localhost"

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg"/> <link rel="icon" href="%PUBLIC_URL%/favicon.svg"/>
<script type="text/javascript" src="%PUBLIC_URL%/config.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta

View File

@ -1,7 +1,7 @@
import { Box, Paper, ThemeProvider, List, ListItem, ListItemButton, ListItemText, SwipeableDrawer, ListItemIcon, IconButton, AppBar, Toolbar, PaletteMode, createTheme, useMediaQuery, useTheme } from "@mui/material"; import { Box, Paper, ThemeProvider, List, ListItem, ListItemButton, ListItemText, SwipeableDrawer, ListItemIcon, IconButton, AppBar, Toolbar, PaletteMode, createTheme, useMediaQuery, useTheme } from "@mui/material";
import React, { Dispatch, ReactNode } from "react"; import React, { Dispatch, ReactNode } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ApiWrapper, getDesignTokens, GlobalUserInfo } from "./common"; import { ApiWrapper, getDesignTokens, GlobalUserInfo } from "./common";
import { LoginPage } from "./login"; import { LoginPage } from "./login";
import ServersBoard from "./servers"; import ServersBoard from "./servers";
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
@ -118,10 +118,10 @@ export default function App() {
return ( return (
<ColorModeContext.Provider value={colorMode}> <ColorModeContext.Provider value={colorMode}>
<Box height={'100vh'} width={'100vw'} overflow='clip' maxHeight='-webkit-fill-available'> <Box height={'100vh'} width={'100vw'} overflow='clip' maxHeight='-webkit-fill-available'>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<BrowserRouter> <BrowserRouter>
<ApiWrapper> <ApiWrapper>
<GlobalUserInfo> <GlobalUserInfo>
<Menu setMode={setMode}> <Menu setMode={setMode}>
<Routes> <Routes>
<Route path='login' element={<LoginPage />} /> <Route path='login' element={<LoginPage />} />
@ -133,10 +133,10 @@ export default function App() {
</Routes> </Routes>
</Menu> </Menu>
</GlobalUserInfo> </GlobalUserInfo>
</ApiWrapper> </ApiWrapper>
</BrowserRouter> </BrowserRouter>
</ThemeProvider> </ThemeProvider>
</Box> </Box>
</ColorModeContext.Provider> </ColorModeContext.Provider>
) )
} }

View File

@ -1,7 +1,7 @@
import axios, { AxiosInstance, AxiosResponse } from "axios"; import axios, { AxiosInstance, AxiosResponse } from "axios";
import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react"; import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react";
import { useLocation, Navigate } from "react-router-dom"; import { useLocation, Navigate } from "react-router-dom";
import Cookies from 'js-cookie' import Cookies from 'js-cookie';
import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material"; import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material";
import { Form } from "@rjsf/mui"; import { Form } from "@rjsf/mui";
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
@ -19,255 +19,255 @@ import { Permission } from "./actions";
import TerminalComponent from "./terminal"; import TerminalComponent from "./terminal";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => {}] as [boolean, Dispatch<boolean>]) export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => { }] as [boolean, Dispatch<boolean>])
export const formModalStyle = { export const formModalStyle = {
position: 'absolute' as 'absolute', position: 'absolute' as 'absolute',
maxHeight: "90%", maxHeight: "90%",
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: '70%', width: '70%',
bgcolor: 'background.paper', bgcolor: 'background.paper',
overflowY: 'auto', overflowY: 'auto',
p: 4, p: 4,
borderRadius: 3, borderRadius: 3,
} }
export const terminalModalStyle = { export const terminalModalStyle = {
position: 'absolute' as 'absolute', position: 'absolute' as 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: '70%', width: '70%',
bgcolor: 'background.paper', bgcolor: 'background.paper',
p: 1, p: 1,
borderRadius: 3, borderRadius: 3,
} }
export const getDesignTokens = (mode: PaletteMode) => ({ export const getDesignTokens = (mode: PaletteMode) => ({
palette: { palette: {
mode, mode,
...(mode === 'light' ...(mode === 'light'
? { ? {
// palette values for light mode // palette values for light mode
primary: blue, primary: blue,
divider: blue[200], divider: blue[200],
background: { background: {
default: grey[200], default: grey[200],
light: grey[100], light: grey[100],
},
text: {
primary: grey[900],
secondary: grey[800],
},
}
: {
// palette values for dark mode
primary: grey,
divider: grey[700],
background: {
default: grey[900],
paper: grey[900],
light: grey[700],
},
text: {
primary: '#fff',
secondary: grey[500],
},
}),
},
components: {
MuiTypography: {
defaultProps: {
color: 'text.primary'
}
}, },
} text: {
primary: grey[900],
secondary: grey[800],
},
}
: {
// palette values for dark mode
primary: grey,
divider: grey[700],
background: {
default: grey[900],
paper: grey[900],
light: grey[700],
},
text: {
primary: '#fff',
secondary: grey[500],
},
}),
},
components: {
MuiTypography: {
defaultProps: {
color: 'text.primary'
}
},
}
}); });
const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_API_URL}` const API_URL = (window as any).API_URL
axios.defaults.withCredentials = true axios.defaults.withCredentials = true
export const api: AxiosInstance = axios.create({ export const api: AxiosInstance = axios.create({
baseURL: API_URL, baseURL: API_URL,
withCredentials: true, withCredentials: true,
}); });
export function ApiWrapper(p: { children: ReactNode}) { export function ApiWrapper(p: { children: ReactNode }) {
const {children} = p const { children } = p
const token = Cookies.get('auth') const token = Cookies.get('auth')
const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token)) const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token))
if (!apiAuthenticated) { if (!apiAuthenticated) {
Cookies.remove('auth') Cookies.remove('auth')
} }
const path = useLocation() const path = useLocation()
return (<apiAuthenticatedContext.Provider value={[apiAuthenticated, setApiAuthenticated]}> return (<apiAuthenticatedContext.Provider value={[apiAuthenticated, setApiAuthenticated]}>
{children} {children}
{!apiAuthenticated && (path.pathname !== '/login' && path.pathname !== '/signup') && <Navigate to='/login' />} {!apiAuthenticated && (path.pathname !== '/login' && path.pathname !== '/signup') && <Navigate to='/login' />}
</apiAuthenticatedContext.Provider>) </apiAuthenticatedContext.Provider>)
} }
export interface ActionInfo { export interface ActionInfo {
name: string name: string
requestType: 'post' | 'get' | 'delete' | 'patch' requestType: 'post' | 'get' | 'delete' | 'patch'
endpoint: string endpoint: string
args: {} args: {}
permissions?: number permissions?: number
response_action?: 'Ignore' | 'Browse' | 'Terminal' response_action?: 'Ignore' | 'Browse' | 'Terminal'
ServerState?: 'on' | 'off' ServerState?: 'on' | 'off'
} }
interface Options{ interface Options {
label: string label: string
const: string const: string
} }
export const actionIdentifierContext: Context<string> = createContext('') export const actionIdentifierContext: Context<string> = createContext('')
function convertNumber(permissions: number): number[]{ function convertNumber(permissions: number): number[] {
var arr: number[] = [] var arr: number[] = []
Object.entries(Permission).forEach( Object.entries(Permission).forEach(
([key, value]) => { ([key, value]) => {
if (permissions&value){ if (permissions & value) {
arr.push(value) arr.push(value)
} }
} }
); );
return arr return arr
} }
function CustomField(props: WidgetProps){ function CustomField(props: WidgetProps) {
const jp = require('jsonpath') const jp = require('jsonpath')
const [options2, setOptions]: [Options[]|null, Dispatch<Options[]>] = useState(null as Options[]|null) const [options2, setOptions]: [Options[] | null, Dispatch<Options[]>] = useState(null as Options[] | null)
const {schema, registry, options, ...newProps} = props const { schema, registry, options, ...newProps } = props
const {SelectWidget, CheckboxesWidget} = registry.widgets const { SelectWidget, CheckboxesWidget } = registry.widgets
if (!schema.fetch_url){ if (!schema.fetch_url) {
if (!schema.permissions){ if (!schema.permissions) {
return <TextField onChange={(event)=>(props.onChange(event.target.value))} value={props.value} label={props.label}/> return <TextField onChange={(event) => (props.onChange(event.target.value))} value={props.value} label={props.label} />
}
return <CheckboxesWidget
{...newProps}
onChange={(event)=>{
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
}
} }
schema={{}}
options={{ return <CheckboxesWidget
{...newProps}
onChange={(event) => {
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
}
}
schema={{}}
options={{
enumOptions: [ enumOptions: [
{label: 'Start', value: Permission.Start}, { label: 'Start', value: Permission.Start },
{label: 'Stop', value: Permission.Stop}, { label: 'Stop', value: Permission.Stop },
{label: 'Browse', value: Permission.Browse}, { label: 'Browse', value: Permission.Browse },
{label: 'Delete', value: Permission.Delete}, { label: 'Delete', value: Permission.Delete },
{label: 'Run Command', value: Permission.RunCommand}, { label: 'Run Command', value: Permission.RunCommand },
{label: 'Create', value: Permission.Create}, { label: 'Create', value: Permission.Create },
{label: 'Admin', value: Permission.Admin}, { label: 'Admin', value: Permission.Admin },
{label: 'Cloud', value: Permission.Cloud}, { label: 'Cloud', value: Permission.Cloud },
]}} registry={registry} value={convertNumber(props.value)} /> ]
} }} registry={registry} value={convertNumber(props.value)} />
}
if (options2 === null){ if (options2 === null) {
api.get(schema.fetch_url as string).then((event)=>{ api.get(schema.fetch_url as string).then((event) => {
let newOptions: Options[] = [] let newOptions: Options[] = []
for (let response of event.data){ for (let response of event.data) {
newOptions.push({ newOptions.push({
const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '), const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '),
label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '), label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '),
})
}
setOptions(newOptions)
}) })
} }
return <SelectWidget {...newProps} schema={{oneOf: options2?options2:[]}} registry={registry} options={{enumOptions: (options2?options2:[]).map((value: Options)=>({label: value.label, value: value.const}))}}/>
setOptions(newOptions)
})
}
return <SelectWidget {...newProps} schema={{ oneOf: options2 ? options2 : [] }} registry={registry} options={{ enumOptions: (options2 ? options2 : []).map((value: Options) => ({ label: value.label, value: value.const })) }} />
} }
function isUserAllowed(user: User|null, action: ActionInfo): boolean{ function isUserAllowed(user: User | null, action: ActionInfo): boolean {
if (user === null){ if (user === null) {
return false
}
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
if (isAdmin){
return true
}
if (!action.permissions){
return true
}
if ((action.permissions & user.Permissions) == action.permissions){
return true
}
return false return false
}
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
if (isAdmin) {
return true
}
if (!action.permissions) {
return true
}
if ((action.permissions & user.Permissions) == action.permissions) {
return true
}
return false
} }
export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string, sx?: ButtonOwnProps, variant?: any, onClick?: Function }) { export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string, sx?: ButtonOwnProps, variant?: any, onClick?: Function }) {
const actionIdentifier: string = useContext(actionIdentifierContext) const actionIdentifier: string = useContext(actionIdentifierContext)
const identifierSubstring = (typeof p.identifierSubstring !== 'undefined') ? p.identifierSubstring : '' const identifierSubstring = (typeof p.identifierSubstring !== 'undefined') ? p.identifierSubstring : ''
const user = useContext(UserInfoContext) const user = useContext(UserInfoContext)
const [form, setForm] = useState(false); const [form, setForm] = useState(false);
const [terminal, setTerminal] = useState(null as string|null); const [terminal, setTerminal] = useState(null as string | null);
const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({}) const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({})
const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier) const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier)
function handleSubmit() { function handleSubmit() {
let promise: Promise<AxiosResponse<any, any>>|null = null let promise: Promise<AxiosResponse<any, any>> | null = null
switch (p.action.requestType) { switch (p.action.requestType) {
case 'post': { case 'post': {
promise = api.post(url, formData) promise = api.post(url, formData)
break break
} }
case 'patch':{ case 'patch': {
promise = api.patch(url, formData) promise = api.patch(url, formData)
break break
} }
case 'get': { case 'get': {
if (formData){ if (formData) {
console.warn('get can get no arguments, dropping') console.warn('get can get no arguments, dropping')
}
promise = api.get(url)
break
}
case 'delete': {
if (formData){
console.warn('delete can get no arguments, dropping')
}
promise = api.delete(url)
break
}
} }
switch (p.action.response_action){ promise = api.get(url)
case 'Browse':{ break
if (promise !== null){ }
promise.then((event)=>{window.open(`https://${event.data}`)}) case 'delete': {
} if (formData) {
} console.warn('delete can get no arguments, dropping')
} }
setForm(false) promise = api.delete(url)
setFormData({}) break
}
} }
switch (p.action.response_action) {
function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) { case 'Browse': {
setFormData(args.formData) if (promise !== null) {
promise.then((event) => { window.open(`https://${event.data}`) })
}
}
} }
setForm(false)
setFormData({})
}
function createTerminal(websocket: string){ function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) {
const NewWindow = window.open('', '', 'width=800 height=600')! setFormData(args.formData)
NewWindow.document.write(`<!doctype html> }
function createTerminal(websocket: string) {
const NewWindow = window.open('', '', 'width=800 height=600')!
NewWindow.document.write(`<!doctype html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="xterm/css/xterm.css" /> <link rel="stylesheet" href="xterm/css/xterm.css" />
@ -329,71 +329,71 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
</body> </body>
</html> </html>
`) `)
NewWindow.onload = () => { NewWindow.onload = () => {
const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement);
root.render(<TerminalComponent websocket={websocket}/>); root.render(<TerminalComponent websocket={websocket} />);
}; };
} }
return (<> return (<>
<Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal'?createTerminal(`ws${API_URL.slice("http".length)}${url}`):setForm(true) }} sx={p.sx}>{p.action.name}</Button > <Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal' ? createTerminal(`ws${API_URL.slice("http".length)}${url}`) : setForm(true) }} sx={p.sx}>{p.action.name}</Button >
<Modal <Modal
onClose={() => { setForm(false); setFormData({}); }} onClose={() => { setForm(false); setFormData({}); }}
open={form} open={form}
> >
<Box sx={{ ...formModalStyle}}> <Box sx={{ ...formModalStyle }}>
<Form validator={validator} widgets={{TextWidget: CustomField}} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} /> <Form validator={validator} widgets={{ TextWidget: CustomField }} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} />
</Box> </Box>
</Modal> </Modal>
<Modal <Modal
onClose={() => { setTerminal(null); }} onClose={() => { setTerminal(null); }}
open={terminal != null} open={terminal != null}
> >
<Box sx={terminalModalStyle}> <Box sx={terminalModalStyle}>
{/* <TerminalComponent websocket={terminal} /> */} {/* <TerminalComponent websocket={terminal} /> */}
</Box> </Box>
</Modal> </Modal>
</>) </>)
} }
export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode}) { export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode }) {
const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} /> )) const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} />))
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null); const anchorRef = React.useRef<HTMLDivElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(0); const [selectedIndex, setSelectedIndex] = React.useState(0);
const user = useContext(UserInfoContext) const user = useContext(UserInfoContext)
for (let child of React.Children.toArray(p.children)){ for (let child of React.Children.toArray(p.children)) {
actionItems.push(child) actionItems.push(child)
}
const handleMenuItemClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
index: number,
) => {
setSelectedIndex(index);
setOpen(false)
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event: Event) => {
if (
anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement)
) {
return;
} }
const handleMenuItemClick = ( setOpen(false);
event: React.MouseEvent<HTMLLIElement, MouseEvent>, }
index: number, return (
) => { <React.Fragment>
setSelectedIndex(index); <ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
setOpen(false) {actionItems[selectedIndex]}
}; <Button
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event: Event) => {
if (
anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement)
) {
return;
}
setOpen(false);
}
return (
<React.Fragment>
<ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
{actionItems[selectedIndex]}
<Button
size="small" size="small"
aria-controls={open ? 'split-button-menu' : undefined} aria-controls={open ? 'split-button-menu' : undefined}
aria-expanded={open ? 'true' : undefined} aria-expanded={open ? 'true' : undefined}
@ -403,87 +403,87 @@ export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: st
> >
<ArrowDropDownIcon /> <ArrowDropDownIcon />
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Popper <Popper
sx={{ sx={{
zIndex: 1, zIndex: 1,
}}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}} }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
> >
{({ TransitionProps, placement }) => ( <Paper>
<Grow <ClickAwayListener onClickAway={handleClose}>
{...TransitionProps} <MenuList id="split-button-menu" autoFocusItem>
style={{ {actionItems.map((option, index) => {
transformOrigin: return <MenuItem
placement === 'bottom' ? 'center top' : 'center bottom', key={option.props.action.name}
}} selected={index === selectedIndex}
> onClick={(event) => handleMenuItemClick(event, index)}
<Paper> disabled={!isUserAllowed(user, option.props.action)}
<ClickAwayListener onClickAway={handleClose}> >
<MenuList id="split-button-menu" autoFocusItem> {option.props.action.name}
{actionItems.map((option, index) => { </MenuItem>
return <MenuItem }
key={option.props.action.name} )
selected={index === selectedIndex} }
onClick={(event) => handleMenuItemClick(event, index)} </MenuList>
disabled={!isUserAllowed(user, option.props.action)} </ClickAwayListener>
> </Paper>
{option.props.action.name} </Grow>
</MenuItem> )}
} </Popper>
) </React.Fragment>
} );
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
);
} }
export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) { export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) {
const { children, headers, actionInfo, actionHook } = props const { children, headers, actionInfo, actionHook } = props
return <Box padding={4} overflow='clip'> return <Box padding={4} overflow='clip'>
<TableContainer component={Paper} sx={{maxHeight: '80svh'}}> <TableContainer component={Paper} sx={{ maxHeight: '80svh' }}>
<Table stickyHeader> <Table stickyHeader>
<TableHead> <TableHead>
<TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}> <TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>
{headers.map((value, index, array) => (<TableCell sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>{value}</TableCell>))} {headers.map((value, index, array) => (<TableCell sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>{value}</TableCell>))}
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{children} {children}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{(actionInfo && <Box marginTop={2} overflow='clip'> {(actionInfo && <Box marginTop={2} overflow='clip'>
<ActionItem variant="contained" action={actionInfo} onClick={actionHook} /> <ActionItem variant="contained" action={actionInfo} onClick={actionHook} />
</Box>)} </Box>)}
</Box> </Box>
} }
export const UserInfoContext: Context<User|null> = createContext(null as User|null) export const UserInfoContext: Context<User | null> = createContext(null as User | null)
export function GlobalUserInfo(props: {children: any}){ export function GlobalUserInfo(props: { children: any }) {
const [user, setUser]: [User|null, Dispatch<User|null>] = useState(null as User|null) const [user, setUser]: [User | null, Dispatch<User | null>] = useState(null as User | null)
const [apiAuthenticated, _] = useContext(apiAuthenticatedContext) const [apiAuthenticated, _] = useContext(apiAuthenticatedContext)
if (user === null && apiAuthenticated){ if (user === null && apiAuthenticated) {
api.get('/users/@me').then((event)=>{setUser(event.data)}).catch(()=>{setUser(null)}) api.get('/users/@me').then((event) => { setUser(event.data) }).catch(() => { setUser(null) })
} }
return <UserInfoContext.Provider value={user}> return <UserInfoContext.Provider value={user}>
{props.children} {props.children}
</UserInfoContext.Provider> </UserInfoContext.Provider>
} }