added terminal resize

This commit is contained in:
ACoolName 2024-05-27 20:38:32 +03:00
parent 340dd92e8a
commit 2dcf21d62e
5 changed files with 19312 additions and 19281 deletions

36096
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +1,59 @@
{ {
"name": "a-cool-games-manager", "name": "a-cool-games-manager",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@rjsf/core": "^5.14.2", "@rjsf/core": "^5.14.2",
"@rjsf/mui": "^5.14.2", "@rjsf/mui": "^5.14.2",
"@rjsf/utils": "^5.14.2", "@rjsf/utils": "^5.14.2",
"@rjsf/validator-ajv8": "^5.14.2", "@rjsf/validator-ajv8": "^5.14.2",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^16.18.63", "@types/node": "^16.18.63",
"@types/react": "^18.2.38", "@types/react": "^18.2.38",
"@types/react-dom": "^18.2.16", "@types/react-dom": "^18.2.16",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"axios": "^1.6.2", "axios": "^1.6.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsonpath": "^1.1.1", "jsonpath": "^1.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-jsonschema-form": "^1.8.1", "react-jsonschema-form": "^1.8.1",
"react-router-dom": "^6.19.0", "react-router-dom": "^6.19.0",
"react-routes": "^0.2.6", "react-routes": "^0.2.6",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"serve": "^14.2.1", "serve": "^14.2.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "/usr/bin/env react-scripts build", "build": "/usr/bin/env react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ]
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
"not dead", "not dead",
"not op_mini all" "not op_mini all"
], ],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,428 +1,428 @@
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';
import { blue, grey } from "@mui/material/colors"; import { blue, grey } from "@mui/material/colors";
import { User } from "./interfaces"; import { User } from "./interfaces";
import { IChangeEvent } from "@rjsf/core"; import { IChangeEvent } from "@rjsf/core";
import { RJSFSchema, WidgetProps } from "@rjsf/utils"; import { RJSFSchema, WidgetProps } from "@rjsf/utils";
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClickAwayListener from '@mui/material/ClickAwayListener'; import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow'; import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper'; import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList'; import MenuList from '@mui/material/MenuList';
import { Permission } from "./actions"; import { Permission } from "./actions";
import TerminalComponent from "./terminal"; import TerminalComponent from "./terminal";
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',
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: 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: 4, p: 4,
borderRadius: 3, borderRadius: 3,
padding: 10, padding: 10,
} }
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: { text: {
primary: grey[900], primary: grey[900],
secondary: grey[800], secondary: grey[800],
}, },
} }
: { : {
// palette values for dark mode // palette values for dark mode
primary: grey, primary: grey,
divider: grey[700], divider: grey[700],
background: { background: {
default: grey[900], default: grey[900],
paper: grey[900], paper: grey[900],
light: grey[700], light: grey[700],
}, },
text: { text: {
primary: '#fff', primary: '#fff',
secondary: grey[500], secondary: grey[500],
}, },
}), }),
}, },
components: { components: {
MuiTypography: { MuiTypography: {
defaultProps: { defaultProps: {
color: 'text.primary' color: 'text.primary'
} }
}, },
} }
}); });
const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_API_URL}` const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_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'
} }
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[] = []
if (permissions&Permission.Start){ if (permissions&Permission.Start){
arr.push(Permission.Start) arr.push(Permission.Start)
} }
if (permissions&Permission.Stop){ if (permissions&Permission.Stop){
arr.push(Permission.Stop) arr.push(Permission.Stop)
} }
if (permissions&Permission.Browse){ if (permissions&Permission.Browse){
arr.push(Permission.Browse) arr.push(Permission.Browse)
} }
if (permissions&Permission.RunCommand){ if (permissions&Permission.RunCommand){
arr.push(Permission.RunCommand) arr.push(Permission.RunCommand)
} }
if (permissions&Permission.Create){ if (permissions&Permission.Create){
arr.push(Permission.Create) arr.push(Permission.Create)
} }
if (permissions&Permission.Delete){ if (permissions&Permission.Delete){
arr.push(Permission.Delete) arr.push(Permission.Delete)
} }
if (permissions&Permission.Admin){ if (permissions&Permission.Admin){
arr.push(Permission.Admin) arr.push(Permission.Admin)
} }
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 return <CheckboxesWidget
{...newProps} {...newProps}
onChange={(event)=>{ onChange={(event)=>{
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0)) props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
} }
} }
schema={{}} schema={{}}
options={{ 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) setOptions(newOptions)
}) })
} }
return <SelectWidget {...newProps} schema={{oneOf: options2?options2:[]}} registry={registry} options={{enumOptions: (options2?options2:[]).map((value: Options)=>({label: value.label, value: value.const}))}}/> 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 return false
} }
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
if (isAdmin){ if (isAdmin){
return true return true
} }
if (!action.permissions){ if (!action.permissions){
return true return true
} }
if ((action.permissions & user.Permissions) == action.permissions){ if ((action.permissions & user.Permissions) == action.permissions){
return true return true
} }
return false 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) promise = api.get(url)
break break
} }
case 'delete': { case 'delete': {
if (formData){ if (formData){
console.warn('delete can get no arguments, dropping') console.warn('delete can get no arguments, dropping')
} }
promise = api.delete(url) promise = api.delete(url)
break break
} }
} }
switch (p.action.response_action){ switch (p.action.response_action){
case 'Browse':{ case 'Browse':{
if (promise !== null){ if (promise !== null){
promise.then((event)=>{window.open(`https://${event.data}`)}) promise.then((event)=>{window.open(`https://${event.data}`)})
} }
} }
} }
setForm(false) setForm(false)
setFormData({}) setFormData({})
} }
function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) { function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) {
setFormData(args.formData) setFormData(args.formData)
} }
return (<> return (<>
<Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal'?setTerminal(`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'?setTerminal(`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 = ( const handleMenuItemClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>, event: React.MouseEvent<HTMLLIElement, MouseEvent>,
index: number, index: number,
) => { ) => {
setSelectedIndex(index); setSelectedIndex(index);
setOpen(false); setOpen(false);
}; };
const handleToggle = () => { const handleToggle = () => {
setOpen((prevOpen) => !prevOpen); setOpen((prevOpen) => !prevOpen);
}; };
const handleClose = (event: Event) => { const handleClose = (event: Event) => {
if ( if (
anchorRef.current && anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement) anchorRef.current.contains(event.target as HTMLElement)
) { ) {
return; return;
} }
setOpen(false); setOpen(false);
} }
return ( return (
<React.Fragment> <React.Fragment>
<ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button"> <ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
{actionItems[selectedIndex]} {actionItems[selectedIndex]}
<Button <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}
aria-label="select merge strategy" aria-label="select merge strategy"
aria-haspopup="menu" aria-haspopup="menu"
onClick={handleToggle} onClick={handleToggle}
> >
<ArrowDropDownIcon /> <ArrowDropDownIcon />
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Popper <Popper
sx={{ sx={{
zIndex: 1, zIndex: 1,
}} }}
open={open} open={open}
anchorEl={anchorRef.current} anchorEl={anchorRef.current}
role={undefined} role={undefined}
transition transition
disablePortal disablePortal
> >
{({ TransitionProps, placement }) => ( {({ TransitionProps, placement }) => (
<Grow <Grow
{...TransitionProps} {...TransitionProps}
style={{ style={{
transformOrigin: transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom', placement === 'bottom' ? 'center top' : 'center bottom',
}} }}
> >
<Paper> <Paper>
<ClickAwayListener onClickAway={handleClose}> <ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" autoFocusItem> <MenuList id="split-button-menu" autoFocusItem>
{actionItems.map((option, index) => { {actionItems.map((option, index) => {
return <MenuItem return <MenuItem
key={option.props.action.name} key={option.props.action.name}
selected={index === selectedIndex} selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)} onClick={(event) => handleMenuItemClick(event, index)}
disabled={!isUserAllowed(user, option.props.action)} disabled={!isUserAllowed(user, option.props.action)}
> >
{option.props.action.name} {option.props.action.name}
</MenuItem> </MenuItem>
} }
) )
} }
</MenuList> </MenuList>
</ClickAwayListener> </ClickAwayListener>
</Paper> </Paper>
</Grow> </Grow>
)} )}
</Popper> </Popper>
</React.Fragment> </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>
} }

View File

@ -1,50 +1,81 @@
// src/components/Terminal.tsx // src/components/Terminal.tsx
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
function TerminalComponent (p: {websocket: string|null}) {
const {websocket} = p const useTerminalResize = (terminalRef: React.RefObject<HTMLDivElement>, fitAddon: FitAddon) => {
const terminalRef = useRef<HTMLDivElement>(null); useEffect(() => {
const terminal = useRef<Terminal | null>(null); const handleResize = () => {
const fitAddon = useRef<FitAddon | null>(null); if (fitAddon && terminalRef.current) {
fitAddon.fit();
useEffect(() => { }
if (terminalRef.current) { };
terminal.current = new Terminal();
fitAddon.current = new FitAddon(); window.addEventListener('resize', handleResize);
terminal.current.loadAddon(fitAddon.current);
terminal.current.open(terminalRef.current); // Fit the terminal initially
fitAddon.current.fit(); handleResize();
if (websocket == null){
return () => {
return ()=>{ window.removeEventListener('resize', handleResize);
terminal.current?.dispose(); };
} }, [terminalRef, fitAddon]);
} };
const socket = new WebSocket(websocket);
function TerminalComponent (p: {websocket: string|null}) {
socket.addEventListener('open', () => { const {websocket} = p
}); const terminalRef = useRef<HTMLDivElement>(null);
const terminal = useRef<Terminal | null>(null);
socket.addEventListener('message', (event) => { const fitAddon = useRef<FitAddon | null>(null);
terminal.current?.write(JSON.parse(event.data));
}); useEffect(() => {
if (terminalRef.current) {
terminal.current.onData((data) => { terminal.current = new Terminal();
socket.send(JSON.stringify({CommandType: 'insert', Arguments: [data]})); fitAddon.current = new FitAddon();
}); terminal.current.loadAddon(fitAddon.current);
terminal.current.open(terminalRef.current);
return () => { fitAddon.current.fit();
terminal.current?.dispose(); if (websocket == null){
socket.close();
}; return ()=>{
} terminal.current?.dispose();
}, []); }
}
return <div ref={terminalRef} style={{ width: '100%', height: '100%' }}></div>;
}; const socket = new WebSocket(websocket);
export default TerminalComponent; socket.send(JSON.stringify({CommandType: 'resize', Arguments: [terminal.current.cols, terminal.current.rows]}))
terminal.current.onResize(
({cols, rows}, _)=>{
socket.send(JSON.stringify({CommandType: 'resize', Arguments: [cols, rows]}))
}
)
socket.addEventListener('open', () => {
});
socket.addEventListener('message', (event) => {
terminal.current?.write(JSON.parse(event.data));
});
terminal.current.onData((data) => {
socket.send(JSON.stringify({CommandType: 'insert', Arguments: [data]}));
});
return () => {
terminal.current?.dispose();
socket.close();
};
}
}, []);
useTerminalResize(terminalRef, fitAddon.current!)
return <div ref={terminalRef} style={{ width: '100%', height: '100%' }}></div>;
};
export default TerminalComponent;