16181: Support SSHHelpHostSuffix, fix user VM page render issues
[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 * as 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         webShell: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
65         ...state.virtualMachines
66     };
67 };
68
69 const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'saveRequestedDate'> => ({
70     saveRequestedDate: () => dispatch<any>(saveRequestedDate()),
71     loadVirtualMachinesData: () => dispatch<any>(loadVirtualMachinesUserData()),
72 });
73
74 interface VirtualMachinesPanelDataProps {
75     requestedDate: string;
76     virtualMachines: ListResults<any>;
77     userUuid: string;
78     links: ListResults<any>;
79     helpText: string;
80     hostSuffix: string;
81     webShell: string;
82 }
83
84 interface VirtualMachinesPanelActionProps {
85     saveRequestedDate: () => void;
86     loadVirtualMachinesData: () => string;
87 }
88
89 type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
90
91 export const VirtualMachineUserPanel = compose(
92     withStyles(styles),
93     connect(mapStateToProps, mapDispatchToProps))(
94         class extends React.Component<VirtualMachineProps> {
95             componentDidMount() {
96                 this.props.loadVirtualMachinesData();
97             }
98
99             render() {
100                 const { virtualMachines, links } = this.props;
101                 return (
102                     <Grid container spacing={16}>
103                         {virtualMachines.itemsAvailable === 0 && <CardContentWithoutVirtualMachines {...this.props} />}
104                         {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
105                         {<CardSSHSection {...this.props} />}
106                     </Grid>
107                 );
108             }
109         }
110     );
111
112 const CardContentWithoutVirtualMachines = (props: VirtualMachineProps) =>
113     <Grid item xs={12}>
114         <Card>
115             <CardContent className={props.classes.cardWithoutMachines}>
116                 <Grid item xs={6}>
117                     <Typography variant='body1'>
118                         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.
119                     </Typography>
120                 </Grid>
121                 <Grid item xs={6} className={props.classes.rightAlign}>
122                     {virtualMachineSendRequest(props)}
123                 </Grid>
124             </CardContent>
125         </Card>
126     </Grid>;
127
128 const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
129     <Grid item xs={12}>
130         <Card>
131             <CardContent>
132                 <span>
133                     <div className={props.classes.rightAlign}>
134                         {virtualMachineSendRequest(props)}
135                     </div>
136                     <div className={props.classes.icon}>
137                         <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
138                             <Tooltip title="Access VM using webshell">
139                                 <HelpIcon />
140                             </Tooltip>
141                         </a>
142                     </div>
143                     {virtualMachinesTable(props)}
144                 </span>
145
146             </CardContent>
147         </Card>
148     </Grid>;
149
150 const virtualMachineSendRequest = (props: VirtualMachineProps) =>
151     <span>
152         <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
153             SEND REQUEST FOR SHELL ACCESS
154         </Button>
155         {props.requestedDate &&
156             <Typography >
157                 A request for shell access was sent on {props.requestedDate}
158             </Typography>}
159     </span>;
160
161 const virtualMachinesTable = (props: VirtualMachineProps) =>
162     <Table>
163         <TableHead>
164             <TableRow>
165                 <TableCell>Host name</TableCell>
166                 <TableCell>Login name</TableCell>
167                 <TableCell>Command line</TableCell>
168                 {props.webShell !== "" && <TableCell>Web shell</TableCell>}
169             </TableRow>
170         </TableHead>
171         <TableBody>
172             {props.virtualMachines.items.map(it =>
173                 props.links.items.map(lk => {
174                     if (lk.tailUuid === props.userUuid) {
175                         const username = lk.properties.username;
176                         const command = `ssh ${username}@${it.hostname}${props.hostSuffix}`;
177                         return <TableRow key={lk.uuid}>
178                             <TableCell>{it.hostname}</TableCell>
179                             <TableCell>{username}</TableCell>
180                             <TableCell>
181                                 {command}
182                             </TableCell>
183                             {props.webShell !== "" && <TableCell>
184                                 <a href={`${props.webShell}${it.href}/webshell/${username}`} target="_blank" className={props.classes.link}>
185                                     Log in as {username}
186                                 </a>
187                             </TableCell>}
188                         </TableRow>;
189                     }
190                     return;
191                 }
192                 ))}
193         </TableBody>
194     </Table>;
195
196 const CardSSHSection = (props: VirtualMachineProps) =>
197     <Grid item xs={12}>
198         <Card>
199             <CardContent>
200                 <Typography>
201                     <div dangerouslySetInnerHTML={{ __html: props.helpText }} style={{ margin: "1em" }} />
202                 </Typography>
203             </CardContent>
204         </Card>
205     </Grid>;