Skip to content

Commit

Permalink
Create server table
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Jan 15, 2024
1 parent ce7abe6 commit 5127a50
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 70 deletions.
70 changes: 70 additions & 0 deletions frontend/src/common/ButtonWithConfirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Button } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import { Fragment, memo, useCallback, useState } from 'react';

interface IButtonWithConfirm {
buttonLabel: string;
dialogTitle: string;
dialogBody: JSX.Element;
action: (() => void) | (() => Promise<void>);
okLabel?: string;
cancelLabel?: string;
}
const Loading = () => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
);
function _ButtonWithConfirm(props: IButtonWithConfirm) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = (
event?: any,
reason?: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason && reason === 'backdropClick') {
return;
}
setOpen(false);
};

const removeEnv = useCallback(async () => {
setLoading(true);
await props.action();
handleClose();
}, [props.action, setLoading]);

return (
<Fragment>
<Button onClick={handleOpen} color="error" size="small">
{props.buttonLabel}
</Button>

<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'sm'}>
<DialogTitle>{props.dialogTitle}</DialogTitle>
<DialogContent>
{!loading && props.dialogBody}
{loading && <Loading />}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
{props.cancelLabel ?? 'Cancel'}
</Button>
<Button variant="contained" color="error" onClick={removeEnv}>
{props.okLabel ?? 'Accept'}
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}

export const ButtonWithConfirm = memo(_ButtonWithConfirm);
21 changes: 21 additions & 0 deletions frontend/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,24 @@
export function encodeUriComponents(uri: string): string {
return uri.split('/').map(encodeURIComponent).join('/');
}

export function formatTime(time: string): string {
const units: { [key: string]: number } = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000
};
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const d1 = new Date(time);
const d2 = new Date();
const elapsed = d1.getTime() - d2.getTime();
for (const u in units) {
if (Math.abs(elapsed) > units[u] || u == 'second') {
return rtf.format(Math.round(elapsed / units[u]), u as any);
}
}
return '';
}
75 changes: 18 additions & 57 deletions frontend/src/environments/RemoveEnvironmentButton.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,20 @@
import { Button, Typography } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import CircularProgress from '@mui/material/CircularProgress';
import { Typography } from '@mui/material';
import Box from '@mui/material/Box';
import { Fragment, memo, useCallback, useState } from 'react';
import { memo, useCallback } from 'react';

import { useAxios } from '../common/AxiosContext';
import { ButtonWithConfirm } from '../common/ButtonWithConfirm';
import { API_PREFIX } from './types';

interface IRemoveEnvironmentButton {
name: string;
image: string;
}
const Loading = () => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
);

function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
const axios = useAxios();
const [open, setOpen] = useState(false);
const [removing, setRemoving] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = (
event?: any,
reason?: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason && reason === 'backdropClick') {
return;
}
setOpen(false);
};

const removeEnv = useCallback(async () => {
setRemoving(true);
const response = await axios.request({
method: 'delete',
path: API_PREFIX,
Expand All @@ -46,39 +23,23 @@ function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
if (response?.status === 'ok') {
window.location.reload();
} else {
handleClose();
}
}, [props.image, axios, setRemoving]);
}, [props.image, axios]);

return (
<Fragment>
<Button onClick={handleOpen} color="error" size="small">
Remove
</Button>

<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'sm'}>
<DialogTitle>Remove environment</DialogTitle>
<DialogContent>
{!removing && (
<Box>
<Typography>
Are you sure you want to remove the following environment?
</Typography>
<pre>{props.name}</pre>
</Box>
)}
{removing && <Loading />}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Cancel
</Button>
<Button variant="contained" color="error" onClick={removeEnv}>
Remove
</Button>
</DialogActions>
</Dialog>
</Fragment>
<ButtonWithConfirm
buttonLabel="Remove"
dialogTitle="Remove environment"
dialogBody={
<Box>
<Typography>
Are you sure you want to remove the following environment?
</Typography>
<pre>{props.name}</pre>
</Box>
}
action={removeEnv}
/>
);
}

Expand Down
37 changes: 37 additions & 0 deletions frontend/src/servers/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Stack } from '@mui/material';
import ScopedCssBaseline from '@mui/material/ScopedCssBaseline';
import { ThemeProvider } from '@mui/material/styles';

import { customTheme } from '../common/theme';
import { IServerData } from './types';
import { AxiosContext } from '../common/AxiosContext';
import { useMemo } from 'react';
import { AxiosClient } from '../common/axiosclient';
import { ServerList } from './ServersList';

export interface IAppProps {
server_data: IServerData[];
allow_named_servers: boolean;
named_server_limit_per_user: number;
}
export default function App(props: IAppProps) {
const axios = useMemo(() => {
const jhData = (window as any).jhdata;
const baseUrl = jhData.base_url;
const xsrfToken = jhData.xsrf_token;
return new AxiosClient({ baseUrl, xsrfToken });
}, []);
console.log('props', props);

return (
<ThemeProvider theme={customTheme}>
<AxiosContext.Provider value={axios}>
<ScopedCssBaseline>
<Stack sx={{ padding: 1 }} spacing={1}>
<ServerList servers={props.server_data} />
</Stack>
</ScopedCssBaseline>
</AxiosContext.Provider>
</ThemeProvider>
);
}
88 changes: 88 additions & 0 deletions frontend/src/servers/ServersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { memo, useMemo } from 'react';

import { Box } from '@mui/system';
import { IServerData } from './types';
import { formatTime } from '../common/utils';

const columns: GridColDef[] = [
{
field: 'name',
headerName: 'Server name',
flex: 1
},
{
field: 'url',
headerName: 'URL',
flex: 1,
renderCell: params => {
return (
<a href={params.value} target="_blank">
{params.value}
</a>
);
}
},
{
field: 'last_activity',
headerName: 'Last activity',
flex: 1,
maxWidth: 150
},
{
field: 'image',
headerName: 'Image',
flex:1,
},
{
field: 'status',
headerName: '',
width: 150,
filterable: false,
sortable: false,
hideable: false,
},
{
field: 'action',
headerName: '',
width: 100,
filterable: false,
sortable: false,
hideable: false
}
];

export interface IServerListProps {
servers: IServerData[];
}

function _ServerList(props: IServerListProps) {
const rows = useMemo(() => {
return props.servers.map((it, id) => {
const newItem: any = { ...it, id };
newItem.image = it.user_options.image ?? '';
newItem.last_activity = formatTime(newItem.last_activity);
return newItem;
});
}, [props]);

return (
<Box sx={{ padding: 1 }}>
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 100
}
}
}}
pageSizeOptions={[100]}
disableRowSelectionOnClick
/>
</Box>
);
}

export const ServerList = memo(_ServerList);
29 changes: 29 additions & 0 deletions frontend/src/servers/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import App, { IAppProps } from './App';

const rootElement = document.getElementById('servers-root');
const root = createRoot(rootElement!);
console.log('AAAAAAAAAA');

const dataElement = document.getElementById('tljh-page-data');
let configData: IAppProps = {
server_data: [],
allow_named_servers: false,
named_server_limit_per_user: 0
};
if (dataElement) {
configData = JSON.parse(dataElement.textContent || '') as IAppProps;
}

root.render(
<StrictMode>
<App {...configData} />
</StrictMode>
);
7 changes: 7 additions & 0 deletions frontend/src/servers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const API_PREFIX = 'spawn';
export interface IServerData {
name: string;
url: string;
last_activity: string;
user_options: { image?: string };
}
11 changes: 10 additions & 1 deletion frontend/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ const environmentsPageConfig = {
},
...config
};
module.exports = [environmentsPageConfig];
const serversPageConfig = {
name: 'servers',
entry: './src/servers/main.tsx',
output: {
path: path.resolve(distRoot, 'react'),
filename: 'servers.js'
},
...config
};
module.exports = [environmentsPageConfig, serversPageConfig];
Loading

0 comments on commit 5127a50

Please sign in to comment.