import React from 'react';
import { connect } from 'react-redux';
-import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core';
+import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip, Chip } from '@material-ui/core';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from 'common/custom-theme';
import { compose, Dispatch } from 'redux';
import { RootState } from 'store/store';
import { ListResults } from 'services/common-service/common-service';
import { HelpIcon } from 'components/icon/icon';
+import { SESSION_STORAGE } from "services/auth-service/auth-service";
// import * as CopyToClipboard from 'react-copy-to-clipboard';
+import parse from "parse-duration";
+import { CopyIcon } from 'components/icon/icon';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
+type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'chipsRoot' | 'copyIcon' | 'tableWrapper' | 'webshellButton';
+
+const EXTRA_TOKEN = "exraToken";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
button: {
icon: {
textAlign: "right",
marginTop: theme.spacing.unit
- }
+ },
+ chipsRoot: {
+ margin: `0px -${theme.spacing.unit / 2}px`,
+ },
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ color: theme.palette.grey["500"],
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem'
+ }
+ },
+ tableWrapper: {
+ overflowX: 'auto',
+ },
+ webshellButton: {
+ textTransform: "initial",
+ },
});
const mapStateToProps = (state: RootState) => {
userUuid: state.auth.user!.uuid,
helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix || "",
- webShell: state.auth.config.clusterConfig.Services.Workbench1.ExternalURL,
+ token: state.auth.extraApiToken || state.auth.apiToken || '',
+ tokenLocation: state.auth.extraApiToken ? EXTRA_TOKEN : (state.auth.apiTokenLocation || ''),
+ webshellUrl: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
+ idleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0,
...state.virtualMachines
};
};
-const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'saveRequestedDate'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'saveRequestedDate' | 'onCopy'> => ({
saveRequestedDate: () => dispatch<any>(saveRequestedDate()),
loadVirtualMachinesData: () => dispatch<any>(loadVirtualMachinesUserData()),
+ onCopy: (message: string) => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ },
});
interface VirtualMachinesPanelDataProps {
links: ListResults<any>;
helpText: string;
hostSuffix: string;
- webShell: string;
+ token: string;
+ tokenLocation: string;
+ webshellUrl: string;
+ idleTimeout: number;
}
interface VirtualMachinesPanelActionProps {
saveRequestedDate: () => void;
loadVirtualMachinesData: () => string;
+ onCopy: (message: string) => void;
}
type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
render() {
const { virtualMachines, links } = this.props;
return (
- <Grid container spacing={16}>
+ <Grid container spacing={16} data-cy="vm-user-panel">
{virtualMachines.itemsAvailable === 0 && <CardContentWithoutVirtualMachines {...this.props} />}
{virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
{<CardSSHSection {...this.props} />}
{virtualMachineSendRequest(props)}
</div>
<div className={props.classes.icon}>
- <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
+ <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" rel="noopener noreferrer" className={props.classes.linkIcon}>
<Tooltip title="Access VM using webshell">
<HelpIcon />
</Tooltip>
</a>
</div>
- {virtualMachinesTable(props)}
+ <div className={props.classes.tableWrapper}>
+ {virtualMachinesTable(props)}
+ </div>
</span>
</CardContent>
</span>;
const virtualMachinesTable = (props: VirtualMachineProps) =>
- <Table>
+ <Table data-cy="vm-user-table">
<TableHead>
<TableRow>
<TableCell>Host name</TableCell>
<TableCell>Login name</TableCell>
+ <TableCell>Groups</TableCell>
<TableCell>Command line</TableCell>
- {props.webShell !== "" && <TableCell>Web shell</TableCell>}
+ <TableCell>Web shell</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.virtualMachines.items.map(it =>
props.links.items.map(lk => {
- if (lk.tailUuid === props.userUuid) {
+ if (lk.tailUuid === props.userUuid && lk.headUuid === it.uuid) {
const username = lk.properties.username;
const command = `ssh ${username}@${it.hostname}${props.hostSuffix}`;
+ let tokenParam = "";
+ if (props.tokenLocation === SESSION_STORAGE || props.tokenLocation === EXTRA_TOKEN) {
+ tokenParam = `&token=${encodeURIComponent(props.token)}`;
+ }
+ const loginHref = `/webshell/?host=${encodeURIComponent(props.webshellUrl + '/' + it.hostname)}&timeout=${props.idleTimeout}&login=${encodeURIComponent(username)}${tokenParam}`;
return <TableRow key={lk.uuid}>
<TableCell>{it.hostname}</TableCell>
<TableCell>{username}</TableCell>
+ <TableCell>
+ <Grid container spacing={8} className={props.classes.chipsRoot}>
+ {
+ (lk.properties.groups || []).map((group, i) => (
+ <Grid item key={i}>
+ <Chip label={group} />
+ </Grid>
+ ))
+ }
+ </Grid>
+ </TableCell>
<TableCell>
{command}
+ <Tooltip title="Copy to clipboard">
+ <span className={props.classes.copyIcon}>
+ <CopyToClipboard text={command || ""} onCopy={() => props.onCopy!("Copied")}>
+ <CopyIcon />
+ </CopyToClipboard>
+ </span>
+ </Tooltip>
+ </TableCell>
+ <TableCell>
+ <Button
+ className={props.classes.webshellButton}
+ variant="contained"
+ size="small"
+ href={loginHref}
+ target="_blank"
+ rel="noopener noreferrer">
+ Log in as {username}
+ </Button>
</TableCell>
- {props.webShell !== "" && <TableCell>
- <a href={`${props.webShell}${it.href}/webshell/${username}`} target="_blank" className={props.classes.link}>
- Log in as {username}
- </a>
- </TableCell>}
</TableRow>;
}
- return;
+ return null;
}
))}
</TableBody>