17229: Add webshell to workbench2
[arvados-workbench2.git] / src / views / virtual-machine-panel / virtual-machine-user-panel.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { connect } from 'react-redux';
7 import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core';
8 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
9 import { ArvadosTheme } from 'common/custom-theme';
10 import { compose, Dispatch } from 'redux';
11 import { saveRequestedDate, loadVirtualMachinesUserData } from 'store/virtual-machines/virtual-machines-actions';
12 import { RootState } from 'store/store';
13 import { ListResults } from 'services/common-service/common-service';
14 import { HelpIcon } from 'components/icon/icon';
15 // import * as CopyToClipboard from 'react-copy-to-clipboard';
16
17 type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
18
19 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
20     button: {
21         marginTop: theme.spacing.unit,
22         marginBottom: theme.spacing.unit
23     },
24     codeSnippet: {
25         borderRadius: theme.spacing.unit * 0.5,
26         border: '1px solid',
27         borderColor: theme.palette.grey["400"],
28     },
29     link: {
30         textDecoration: 'none',
31         color: theme.palette.primary.main,
32         "&:hover": {
33             color: theme.palette.primary.dark,
34             transition: 'all 0.5s ease'
35         }
36     },
37     linkIcon: {
38         textDecoration: 'none',
39         color: theme.palette.grey["500"],
40         textAlign: 'right',
41         "&:hover": {
42             color: theme.palette.common.black,
43             transition: 'all 0.5s ease'
44         }
45     },
46     rightAlign: {
47         textAlign: "right"
48     },
49     cardWithoutMachines: {
50         display: 'flex'
51     },
52     icon: {
53         textAlign: "right",
54         marginTop: theme.spacing.unit
55     }
56 });
57
58 const mapStateToProps = (state: RootState) => {
59     return {
60         requestedDate: state.virtualMachines.date,
61         userUuid: state.auth.user!.uuid,
62         helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
63         hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix || "",
64         token: state.auth.extraApiToken || state.auth.apiToken || '',
65         webshellUrl: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
66         ...state.virtualMachines
67     };
68 };
69
70 const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'saveRequestedDate'> => ({
71     saveRequestedDate: () => dispatch<any>(saveRequestedDate()),
72     loadVirtualMachinesData: () => dispatch<any>(loadVirtualMachinesUserData()),
73 });
74
75 interface VirtualMachinesPanelDataProps {
76     requestedDate: string;
77     virtualMachines: ListResults<any>;
78     userUuid: string;
79     links: ListResults<any>;
80     helpText: string;
81     hostSuffix: string;
82     token: string;
83     webshellUrl: string;
84 }
85
86 interface VirtualMachinesPanelActionProps {
87     saveRequestedDate: () => void;
88     loadVirtualMachinesData: () => string;
89 }
90
91 type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
92
93 export const VirtualMachineUserPanel = compose(
94     withStyles(styles),
95     connect(mapStateToProps, mapDispatchToProps))(
96         class extends React.Component<VirtualMachineProps> {
97             componentDidMount() {
98                 this.props.loadVirtualMachinesData();
99             }
100
101             render() {
102                 const { virtualMachines, links } = this.props;
103                 return (
104                     <Grid container spacing={16}>
105                         {virtualMachines.itemsAvailable === 0 && <CardContentWithoutVirtualMachines {...this.props} />}
106                         {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
107                         {<CardSSHSection {...this.props} />}
108                     </Grid>
109                 );
110             }
111         }
112     );
113
114 const CardContentWithoutVirtualMachines = (props: VirtualMachineProps) =>
115     <Grid item xs={12}>
116         <Card>
117             <CardContent className={props.classes.cardWithoutMachines}>
118                 <Grid item xs={6}>
119                     <Typography variant='body1'>
120                         You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
121                     </Typography>
122                 </Grid>
123                 <Grid item xs={6} className={props.classes.rightAlign}>
124                     {virtualMachineSendRequest(props)}
125                 </Grid>
126             </CardContent>
127         </Card>
128     </Grid>;
129
130 const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
131     <Grid item xs={12}>
132         <Card>
133             <CardContent>
134                 <span>
135                     <div className={props.classes.rightAlign}>
136                         {virtualMachineSendRequest(props)}
137                     </div>
138                     <div className={props.classes.icon}>
139                         <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" rel="noopener noreferrer" className={props.classes.linkIcon}>
140                             <Tooltip title="Access VM using webshell">
141                                 <HelpIcon />
142                             </Tooltip>
143                         </a>
144                     </div>
145                     {virtualMachinesTable(props)}
146                 </span>
147
148             </CardContent>
149         </Card>
150     </Grid>;
151
152 const virtualMachineSendRequest = (props: VirtualMachineProps) =>
153     <span>
154         <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
155             SEND REQUEST FOR SHELL ACCESS
156         </Button>
157         {props.requestedDate &&
158             <Typography >
159                 A request for shell access was sent on {props.requestedDate}
160             </Typography>}
161     </span>;
162
163 const virtualMachinesTable = (props: VirtualMachineProps) =>
164     <Table>
165         <TableHead>
166             <TableRow>
167                 <TableCell>Host name</TableCell>
168                 <TableCell>Login name</TableCell>
169                 <TableCell>Command line</TableCell>
170                 <TableCell>Web shell</TableCell>
171             </TableRow>
172         </TableHead>
173         <TableBody>
174             {props.virtualMachines.items.map(it =>
175                 props.links.items.map(lk => {
176                     if (lk.tailUuid === props.userUuid) {
177                         const username = lk.properties.username;
178                         const command = `ssh ${username}@${it.hostname}${props.hostSuffix}`;
179                         return <TableRow key={lk.uuid}>
180                             <TableCell>{it.hostname}</TableCell>
181                             <TableCell>{username}</TableCell>
182                             <TableCell>
183                                 {command}
184                             </TableCell>
185                             <TableCell>
186                                 <a href={`/webshell/?host=${encodeURIComponent(props.webshellUrl + '/' + it.hostname)}&login=${username}&token=${encodeURIComponent(props.token)}`} target="_blank" rel="noopener noreferrer" className={props.classes.link}>
187                                     Log in as {username}
188                                 </a>
189                             </TableCell>
190                         </TableRow>;
191                     }
192                     return null;
193                 }
194                 ))}
195         </TableBody>
196     </Table>;
197
198 const CardSSHSection = (props: VirtualMachineProps) =>
199     <Grid item xs={12}>
200         <Card>
201             <CardContent>
202                 <Typography>
203                     <div dangerouslySetInnerHTML={{ __html: props.helpText }} style={{ margin: "1em" }} />
204                 </Typography>
205             </CardContent>
206         </Card>
207     </Grid>;