Skip to content

Commit 5127a50

Browse files
committed
Create server table
1 parent ce7abe6 commit 5127a50

File tree

11 files changed

+296
-70
lines changed

11 files changed

+296
-70
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Button } from '@mui/material';
2+
import Dialog from '@mui/material/Dialog';
3+
import DialogActions from '@mui/material/DialogActions';
4+
import DialogContent from '@mui/material/DialogContent';
5+
import DialogTitle from '@mui/material/DialogTitle';
6+
import CircularProgress from '@mui/material/CircularProgress';
7+
import Box from '@mui/material/Box';
8+
import { Fragment, memo, useCallback, useState } from 'react';
9+
10+
interface IButtonWithConfirm {
11+
buttonLabel: string;
12+
dialogTitle: string;
13+
dialogBody: JSX.Element;
14+
action: (() => void) | (() => Promise<void>);
15+
okLabel?: string;
16+
cancelLabel?: string;
17+
}
18+
const Loading = () => (
19+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
20+
<CircularProgress />
21+
</Box>
22+
);
23+
function _ButtonWithConfirm(props: IButtonWithConfirm) {
24+
const [open, setOpen] = useState(false);
25+
const [loading, setLoading] = useState(false);
26+
const handleOpen = () => {
27+
setOpen(true);
28+
};
29+
const handleClose = (
30+
event?: any,
31+
reason?: 'backdropClick' | 'escapeKeyDown'
32+
) => {
33+
if (reason && reason === 'backdropClick') {
34+
return;
35+
}
36+
setOpen(false);
37+
};
38+
39+
const removeEnv = useCallback(async () => {
40+
setLoading(true);
41+
await props.action();
42+
handleClose();
43+
}, [props.action, setLoading]);
44+
45+
return (
46+
<Fragment>
47+
<Button onClick={handleOpen} color="error" size="small">
48+
{props.buttonLabel}
49+
</Button>
50+
51+
<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'sm'}>
52+
<DialogTitle>{props.dialogTitle}</DialogTitle>
53+
<DialogContent>
54+
{!loading && props.dialogBody}
55+
{loading && <Loading />}
56+
</DialogContent>
57+
<DialogActions>
58+
<Button variant="contained" onClick={handleClose}>
59+
{props.cancelLabel ?? 'Cancel'}
60+
</Button>
61+
<Button variant="contained" color="error" onClick={removeEnv}>
62+
{props.okLabel ?? 'Accept'}
63+
</Button>
64+
</DialogActions>
65+
</Dialog>
66+
</Fragment>
67+
);
68+
}
69+
70+
export const ButtonWithConfirm = memo(_ButtonWithConfirm);

frontend/src/common/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,24 @@
99
export function encodeUriComponents(uri: string): string {
1010
return uri.split('/').map(encodeURIComponent).join('/');
1111
}
12+
13+
export function formatTime(time: string): string {
14+
const units: { [key: string]: number } = {
15+
year: 24 * 60 * 60 * 1000 * 365,
16+
month: (24 * 60 * 60 * 1000 * 365) / 12,
17+
day: 24 * 60 * 60 * 1000,
18+
hour: 60 * 60 * 1000,
19+
minute: 60 * 1000,
20+
second: 1000
21+
};
22+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
23+
const d1 = new Date(time);
24+
const d2 = new Date();
25+
const elapsed = d1.getTime() - d2.getTime();
26+
for (const u in units) {
27+
if (Math.abs(elapsed) > units[u] || u == 'second') {
28+
return rtf.format(Math.round(elapsed / units[u]), u as any);
29+
}
30+
}
31+
return '';
32+
}

frontend/src/environments/RemoveEnvironmentButton.tsx

Lines changed: 18 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,20 @@
1-
import { Button, Typography } from '@mui/material';
2-
import Dialog from '@mui/material/Dialog';
3-
import DialogActions from '@mui/material/DialogActions';
4-
import DialogContent from '@mui/material/DialogContent';
5-
import DialogTitle from '@mui/material/DialogTitle';
6-
import CircularProgress from '@mui/material/CircularProgress';
1+
import { Typography } from '@mui/material';
72
import Box from '@mui/material/Box';
8-
import { Fragment, memo, useCallback, useState } from 'react';
3+
import { memo, useCallback } from 'react';
94

105
import { useAxios } from '../common/AxiosContext';
6+
import { ButtonWithConfirm } from '../common/ButtonWithConfirm';
117
import { API_PREFIX } from './types';
128

139
interface IRemoveEnvironmentButton {
1410
name: string;
1511
image: string;
1612
}
17-
const Loading = () => (
18-
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
19-
<CircularProgress />
20-
</Box>
21-
);
13+
2214
function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
2315
const axios = useAxios();
24-
const [open, setOpen] = useState(false);
25-
const [removing, setRemoving] = useState(false);
26-
const handleOpen = () => {
27-
setOpen(true);
28-
};
29-
const handleClose = (
30-
event?: any,
31-
reason?: 'backdropClick' | 'escapeKeyDown'
32-
) => {
33-
if (reason && reason === 'backdropClick') {
34-
return;
35-
}
36-
setOpen(false);
37-
};
3816

3917
const removeEnv = useCallback(async () => {
40-
setRemoving(true);
4118
const response = await axios.request({
4219
method: 'delete',
4320
path: API_PREFIX,
@@ -46,39 +23,23 @@ function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
4623
if (response?.status === 'ok') {
4724
window.location.reload();
4825
} else {
49-
handleClose();
5026
}
51-
}, [props.image, axios, setRemoving]);
27+
}, [props.image, axios]);
5228

5329
return (
54-
<Fragment>
55-
<Button onClick={handleOpen} color="error" size="small">
56-
Remove
57-
</Button>
58-
59-
<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'sm'}>
60-
<DialogTitle>Remove environment</DialogTitle>
61-
<DialogContent>
62-
{!removing && (
63-
<Box>
64-
<Typography>
65-
Are you sure you want to remove the following environment?
66-
</Typography>
67-
<pre>{props.name}</pre>
68-
</Box>
69-
)}
70-
{removing && <Loading />}
71-
</DialogContent>
72-
<DialogActions>
73-
<Button variant="contained" onClick={handleClose}>
74-
Cancel
75-
</Button>
76-
<Button variant="contained" color="error" onClick={removeEnv}>
77-
Remove
78-
</Button>
79-
</DialogActions>
80-
</Dialog>
81-
</Fragment>
30+
<ButtonWithConfirm
31+
buttonLabel="Remove"
32+
dialogTitle="Remove environment"
33+
dialogBody={
34+
<Box>
35+
<Typography>
36+
Are you sure you want to remove the following environment?
37+
</Typography>
38+
<pre>{props.name}</pre>
39+
</Box>
40+
}
41+
action={removeEnv}
42+
/>
8243
);
8344
}
8445

frontend/src/servers/App.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Stack } from '@mui/material';
2+
import ScopedCssBaseline from '@mui/material/ScopedCssBaseline';
3+
import { ThemeProvider } from '@mui/material/styles';
4+
5+
import { customTheme } from '../common/theme';
6+
import { IServerData } from './types';
7+
import { AxiosContext } from '../common/AxiosContext';
8+
import { useMemo } from 'react';
9+
import { AxiosClient } from '../common/axiosclient';
10+
import { ServerList } from './ServersList';
11+
12+
export interface IAppProps {
13+
server_data: IServerData[];
14+
allow_named_servers: boolean;
15+
named_server_limit_per_user: number;
16+
}
17+
export default function App(props: IAppProps) {
18+
const axios = useMemo(() => {
19+
const jhData = (window as any).jhdata;
20+
const baseUrl = jhData.base_url;
21+
const xsrfToken = jhData.xsrf_token;
22+
return new AxiosClient({ baseUrl, xsrfToken });
23+
}, []);
24+
console.log('props', props);
25+
26+
return (
27+
<ThemeProvider theme={customTheme}>
28+
<AxiosContext.Provider value={axios}>
29+
<ScopedCssBaseline>
30+
<Stack sx={{ padding: 1 }} spacing={1}>
31+
<ServerList servers={props.server_data} />
32+
</Stack>
33+
</ScopedCssBaseline>
34+
</AxiosContext.Provider>
35+
</ThemeProvider>
36+
);
37+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { DataGrid, GridColDef } from '@mui/x-data-grid';
2+
import { memo, useMemo } from 'react';
3+
4+
import { Box } from '@mui/system';
5+
import { IServerData } from './types';
6+
import { formatTime } from '../common/utils';
7+
8+
const columns: GridColDef[] = [
9+
{
10+
field: 'name',
11+
headerName: 'Server name',
12+
flex: 1
13+
},
14+
{
15+
field: 'url',
16+
headerName: 'URL',
17+
flex: 1,
18+
renderCell: params => {
19+
return (
20+
<a href={params.value} target="_blank">
21+
{params.value}
22+
</a>
23+
);
24+
}
25+
},
26+
{
27+
field: 'last_activity',
28+
headerName: 'Last activity',
29+
flex: 1,
30+
maxWidth: 150
31+
},
32+
{
33+
field: 'image',
34+
headerName: 'Image',
35+
flex:1,
36+
},
37+
{
38+
field: 'status',
39+
headerName: '',
40+
width: 150,
41+
filterable: false,
42+
sortable: false,
43+
hideable: false,
44+
},
45+
{
46+
field: 'action',
47+
headerName: '',
48+
width: 100,
49+
filterable: false,
50+
sortable: false,
51+
hideable: false
52+
}
53+
];
54+
55+
export interface IServerListProps {
56+
servers: IServerData[];
57+
}
58+
59+
function _ServerList(props: IServerListProps) {
60+
const rows = useMemo(() => {
61+
return props.servers.map((it, id) => {
62+
const newItem: any = { ...it, id };
63+
newItem.image = it.user_options.image ?? '';
64+
newItem.last_activity = formatTime(newItem.last_activity);
65+
return newItem;
66+
});
67+
}, [props]);
68+
69+
return (
70+
<Box sx={{ padding: 1 }}>
71+
<DataGrid
72+
rows={rows}
73+
columns={columns}
74+
initialState={{
75+
pagination: {
76+
paginationModel: {
77+
pageSize: 100
78+
}
79+
}
80+
}}
81+
pageSizeOptions={[100]}
82+
disableRowSelectionOnClick
83+
/>
84+
</Box>
85+
);
86+
}
87+
88+
export const ServerList = memo(_ServerList);

frontend/src/servers/main.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import '@fontsource/roboto/300.css';
2+
import '@fontsource/roboto/400.css';
3+
import '@fontsource/roboto/500.css';
4+
import '@fontsource/roboto/700.css';
5+
6+
import { StrictMode } from 'react';
7+
import { createRoot } from 'react-dom/client';
8+
9+
import App, { IAppProps } from './App';
10+
11+
const rootElement = document.getElementById('servers-root');
12+
const root = createRoot(rootElement!);
13+
console.log('AAAAAAAAAA');
14+
15+
const dataElement = document.getElementById('tljh-page-data');
16+
let configData: IAppProps = {
17+
server_data: [],
18+
allow_named_servers: false,
19+
named_server_limit_per_user: 0
20+
};
21+
if (dataElement) {
22+
configData = JSON.parse(dataElement.textContent || '') as IAppProps;
23+
}
24+
25+
root.render(
26+
<StrictMode>
27+
<App {...configData} />
28+
</StrictMode>
29+
);

frontend/src/servers/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const API_PREFIX = 'spawn';
2+
export interface IServerData {
3+
name: string;
4+
url: string;
5+
last_activity: string;
6+
user_options: { image?: string };
7+
}

frontend/webpack.config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,13 @@ const environmentsPageConfig = {
3838
},
3939
...config
4040
};
41-
module.exports = [environmentsPageConfig];
41+
const serversPageConfig = {
42+
name: 'servers',
43+
entry: './src/servers/main.tsx',
44+
output: {
45+
path: path.resolve(distRoot, 'react'),
46+
filename: 'servers.js'
47+
},
48+
...config
49+
};
50+
module.exports = [environmentsPageConfig, serversPageConfig];

0 commit comments

Comments
 (0)