Skip to content

Commit

Permalink
Add terminal
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Jan 12, 2024
1 parent 4dcb6ea commit 3671907
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 27 deletions.
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
"axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"url-join": "^5.0.0"
"url-join": "^5.0.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"eslintIgnore": [
"node_modules",
Expand Down
37 changes: 15 additions & 22 deletions frontend/src/environments/EnvironmentList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IconButton } from '@mui/material';
// import { IconButton } from '@mui/material';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { IEnvironmentData } from './types';
import { memo, useMemo } from 'react';
import CheckIcon from '@mui/icons-material/Check';
import SyncIcon from '@mui/icons-material/Sync';
// import CheckIcon from '@mui/icons-material/Check';

import { Box } from '@mui/system';
import { RemoveEnvironmentButton } from './RemoveEnvironmentButton';
import { EnvironmentLogButton } from './LogDialog';
const columns: GridColDef[] = [
{
field: 'display_name',
Expand Down Expand Up @@ -51,26 +52,18 @@ const columns: GridColDef[] = [
hideSortIcons: true,
renderCell: params => {
return params.value === 'built' ? (
<IconButton>
<CheckIcon color="success" />
</IconButton>
// <IconButton>
// <CheckIcon color="success" />
// </IconButton>
<EnvironmentLogButton
name={params.row.display_name}
image={params.row.image_name}
/>
) : params.value === 'building' ? (
<IconButton>
<SyncIcon
sx={{
animation: 'spin 2s linear infinite',
'@keyframes spin': {
'0%': {
transform: 'rotate(360deg)'
},
'100%': {
transform: 'rotate(0deg)'
}
}
}}
htmlColor="orange"
/>
</IconButton>
<EnvironmentLogButton
name={params.row.display_name}
image={params.row.image_name}
/>
) : null;
}
},
Expand Down
125 changes: 125 additions & 0 deletions frontend/src/environments/LogDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Button, IconButton } 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 { Fragment, memo, useRef, useState } from 'react';
import SyncIcon from '@mui/icons-material/Sync';
import 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import urlJoin from 'url-join';

interface IRemoveEnvironmentButton {
name: string;
image: string;
}

const terminalFactory = () => {
const terminal = new Terminal();
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
return { terminal, fitAddon };
};

function _EnvironmentLogButton(props: IRemoveEnvironmentButton) {
const [open, setOpen] = useState(false);
const divRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<{ terminal: Terminal; fitAddon: FitAddon }>(
terminalFactory()
);
const handleOpen = () => {
setOpen(true);
if (divRef.current) {
terminalRef.current.terminal.open(divRef.current);
terminalRef.current.fitAddon.fit();
const jhData = (window as any).jhdata;
const baseUrl = jhData.base_url;
const xsrfToken = jhData.xsrf_token;
let logsUrl = urlJoin(
baseUrl,
'api',
'environments',
props.image,
'logs'
);
if (xsrfToken) {
// add xsrf token to url parameter
const sep = logsUrl.indexOf('?') === -1 ? '?' : '&';
logsUrl = logsUrl + sep + '_xsrf=' + xsrfToken;
}
const eventSource = new EventSource(logsUrl);
eventSource.onerror = err => {
console.error('Failed to construct event stream', err);
eventSource.close();
};

eventSource.onmessage = event => {
var data = JSON.parse(event.data);
console.log('data', data);

if (data.phase === 'built') {
eventSource.close();
return;
}
terminalRef.current.terminal.write(data.message);
terminalRef.current.fitAddon.fit();
};
}
};
const handleClose = (
event?: any,
reason?: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason && reason === 'backdropClick') {
return;
}

terminalRef.current.terminal.dispose();
if (divRef.current) {
divRef.current.innerHTML = '';
}
setOpen(false);
};

return (
<Fragment>
<IconButton onClick={handleOpen}>
<SyncIcon
sx={{
animation: 'spin 2s linear infinite',
'@keyframes spin': {
'0%': {
transform: 'rotate(360deg)'
},
'100%': {
transform: 'rotate(0deg)'
}
}
}}
htmlColor="orange"
/>
</IconButton>

<Dialog
open={open}
onClose={handleClose}
fullWidth
maxWidth={'sm'}
keepMounted={true}
>
<DialogTitle>Creating environment {props.name}</DialogTitle>
<DialogContent>
<div ref={divRef} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}

export const EnvironmentLogButton = memo(_EnvironmentLogButton);
15 changes: 11 additions & 4 deletions frontend/src/environments/RemoveEnvironmentButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Button, Typography } from '@mui/material';
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';

import { useAxios } from '../common/AxiosContext';
Expand All @@ -12,7 +14,11 @@ interface IRemoveEnvironmentButton {
name: string;
image: string;
}

const Loading = () => (
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
);
function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
const axios = useAxios();
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -51,10 +57,11 @@ function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'sm'}>
<DialogTitle>Remove environment</DialogTitle>
<DialogContent>
<Typography>
{/* <Typography>
Are you sure you want to remove the following environment?
</Typography>
<pre>{props.name}</pre>
<pre>{props.name}</pre> */}
<Loading />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Expand Down
18 changes: 18 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4907,6 +4907,8 @@ __metadata:
url-join: ^5.0.0
webpack: ^5.74.0
webpack-cli: ^5.1.4
xterm: ^5.3.0
xterm-addon-fit: ^0.8.0
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -5349,6 +5351,22 @@ __metadata:
languageName: node
linkType: hard

"xterm-addon-fit@npm:^0.8.0":
version: 0.8.0
resolution: "xterm-addon-fit@npm:0.8.0"
peerDependencies:
xterm: ^5.0.0
checksum: 5af2041b442f7c804eda2e6f62e3b68b5159b0ae6bd96e2aa8d85b26441df57291cbfed653d1196d4af5d9b94bfc39993df8b409a25c35e0d36bdaf6f5cdfe5f
languageName: node
linkType: hard

"xterm@npm:^5.3.0":
version: 5.3.0
resolution: "xterm@npm:5.3.0"
checksum: 1bdfdfe4cae4412128376180d85e476b43fb021cdd1114b18acad821c9ea44b5b600e0d88febf2b3572f38fad7741e5161ce0178a44369617cf937222cc6e011
languageName: node
linkType: hard

"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"
Expand Down

0 comments on commit 3671907

Please sign in to comment.