// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import AccessTime from '@material-ui/icons/AccessTime';
import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
-export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
export const RenameIcon: IconType = (props) => <Edit {...props} />;
// SPDX-License-Identifier: AGPL-3.0
import { Resource } from "./resource";
+import { TagProperty } from "~/models/tag";
export interface LinkResource extends Resource {
headUuid: string;
tailUuid: string;
linkClass: string;
name: string;
- properties: {};
+ properties: TagProperty;
}
export enum LinkClass {
REPOSITORY = "arvados#repository",
SSH_KEY = "arvados#authorizedKeys",
USER = "arvados#user",
+ VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
NONE = "arvados#none"
}
LOG = '57u5n',
REPOSITORY = 's0uqq',
USER = 'tpzed',
+ VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
SSH_KEY = 'fngyi'
}
return ResourceKind.LOG;
case ResourceObjectType.WORKFLOW:
return ResourceKind.WORKFLOW;
+ case ResourceObjectType.VIRTUAL_MACHINE:
+ return ResourceKind.VIRTUAL_MACHINE;
case ResourceObjectType.REPOSITORY:
return ResourceKind.REPOSITORY;
case ResourceObjectType.SSH_KEY:
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+
+export interface VirtualMachinesResource extends Resource {
+ hostname: string;
+}
+
+export interface VirtualMachinesLoginsResource {
+ hostname: string;
+ username: string;
+ public_key: string;
+ user_uuid: string;
+ virtual_machine_uuid: string;
+ authorized_key_uuid: string;
+}
\ No newline at end of file
import { History, Location } from 'history';
import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories } from '~/store/workbench/workbench-actions';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from './routes';
+import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadVirtualMachines } from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions';
const searchResultsMatch = matchSearchResultsRoute(pathname);
const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
const runProcessMatch = matchRunProcessRoute(pathname);
+ const virtualMachineMatch = matchVirtualMachineRoute(pathname);
const workflowMatch = matchWorkflowRoute(pathname);
const sshKeysMatch = matchSshKeysRoute(pathname);
store.dispatch(loadWorkflow);
} else if (searchResultsMatch) {
store.dispatch(loadSearchResults);
+ } else if (virtualMachineMatch) {
+ store.dispatch(loadVirtualMachines);
} else if(repositoryMatch) {
store.dispatch(loadRepositories);
} else if (sshKeysMatch) {
REPOSITORIES: '/repositories',
SHARED_WITH_ME: '/shared-with-me',
RUN_PROCESS: '/run-process',
+ VIRTUAL_MACHINES: '/virtual-machines',
WORKFLOWS: '/workflows',
SEARCH_RESULTS: '/search-results',
SSH_KEYS: `/ssh-keys`
export const matchSearchResultsRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
+export const matchVirtualMachineRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
+
export const matchRepositoriesRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
import { WorkflowService } from "~/services/workflow-service/workflow-service";
import { SearchService } from '~/services/search-service/search-service';
import { PermissionService } from "~/services/permission-service/permission-service";
+import { VirtualMachinesService } from "~/services/virtual-machines-service/virtual-machines-service";
import { RepositoriesService } from '~/services/repositories-service/repositories-service';
import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
const projectService = new ProjectService(apiClient, actions);
const repositoriesService = new RepositoriesService(apiClient, actions);
const userService = new UserService(apiClient, actions);
+ const virtualMachineService = new VirtualMachinesService(apiClient, actions);
const workflowService = new WorkflowService(apiClient, actions);
const ancestorsService = new AncestorService(groupsService, userService);
searchService,
tagService,
userService,
+ virtualMachineService,
webdavClient,
workflowService,
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { VirtualMachinesResource } from '~/models/virtual-machines';
+import { ApiActions } from '~/services/api/api-actions';
+
+export class VirtualMachinesService extends CommonResourceService<VirtualMachinesResource> {
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "virtual_machines", actions);
+ }
+
+ getRequestedDate(): string {
+ return localStorage.getItem('requestedDate') || '';
+ }
+
+ saveRequestedDate(date: string) {
+ localStorage.setItem('requestedDate', date);
+ }
+
+ logins(uuid: string) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get(`virtual_machines/${uuid}/logins`),
+ this.actions
+ );
+ }
+
+ getAllLogins() {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get('virtual_machines/get_all_logins'),
+ this.actions
+ );
+ }
+}
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { getProperty } from '../properties/properties';
+import { getProperty } from '~/store/properties/properties';
import { propertiesActions } from '~/store/properties/properties-actions';
import { RootState } from '~/store/store';
export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
+export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
+
export const navigateToRepositories = push(Routes.REPOSITORIES);
export const navigateToSshKeys= push(Routes.SSH_KEYS);
PROJECTS = 'Projects',
SHARED_WITH_ME = 'Shared with me',
WORKFLOWS = 'Workflows',
- RECENT_OPEN = 'Recently open',
FAVORITES = 'Favorites',
TRASH = 'Trash'
}
const SIDE_PANEL_CATEGORIES = [
SidePanelTreeCategory.WORKFLOWS,
- SidePanelTreeCategory.RECENT_OPEN,
SidePanelTreeCategory.FAVORITES,
SidePanelTreeCategory.TRASH,
];
import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer";
+import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
const composeEnhancers =
runProcessPanel: runProcessPanelReducer,
appInfo: appInfoReducer,
searchBar: searchBarReducer,
+ virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { ServiceRepository } from "~/services/services";
+import { navigateToVirtualMachines } from "../navigation/navigation-action";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { formatDate } from "~/common/formatters";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { VirtualMachinesLoginsResource } from '~/models/virtual-machines';
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { ListResults } from "~/services/common-service/common-resource-service";
+
+export const virtualMachinesActions = unionize({
+ SET_REQUESTED_DATE: ofType<string>(),
+ SET_VIRTUAL_MACHINES: ofType<ListResults<any>>(),
+ SET_LOGINS: ofType<VirtualMachinesLoginsResource[]>(),
+ SET_LINKS: ofType<ListResults<any>>()
+});
+
+export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
+
+export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
+
+export const openVirtualMachines = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(navigateToVirtualMachines);
+ };
+
+const loadRequestedDate = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const date = services.virtualMachineService.getRequestedDate();
+ dispatch(virtualMachinesActions.SET_REQUESTED_DATE(date));
+ };
+
+
+export const loadVirtualMachinesData = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(loadRequestedDate());
+ const virtualMachines = await services.virtualMachineService.list();
+ const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
+ const links = await services.linkService.list({
+ filters: new FilterBuilder()
+ .addIn("headUuid", virtualMachinesUuids)
+ .getFilters()
+ });
+ dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
+ dispatch(virtualMachinesActions.SET_LINKS(links));
+ };
+
+export const saveRequestedDate = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const date = formatDate((new Date).toISOString());
+ services.virtualMachineService.saveRequestedDate(date);
+ dispatch<any>(loadRequestedDate());
+ };
+
+const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
+
+export const loadVirtualMachinesPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(virtualMachinesBindedActions.REQUEST_ITEMS());
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { virtualMachinesActions, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions';
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { VirtualMachinesLoginsResource } from '~/models/virtual-machines';
+
+interface VirtualMachines {
+ date: string;
+ virtualMachines: ListResults<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+const initialState: VirtualMachines = {
+ date: '',
+ virtualMachines: {
+ kind: '',
+ offset: 0,
+ limit: 0,
+ itemsAvailable: 0,
+ items: []
+ },
+ logins: [],
+ links: {
+ kind: '',
+ offset: 0,
+ limit: 0,
+ itemsAvailable: 0,
+ items: []
+ }
+};
+
+export const virtualMachinesReducer = (state = initialState, action: VirtualMachineActions): VirtualMachines =>
+ virtualMachinesActions.match(action, {
+ SET_REQUESTED_DATE: date => ({ ...state, date }),
+ SET_VIRTUAL_MACHINES: virtualMachines => ({ ...state, virtualMachines }),
+ SET_LOGINS: logins => ({ ...state, logins }),
+ SET_LINKS: links => ({ ...state, links }),
+ default: () => state
+ });
import { CollectionResource } from "~/models/collection";
import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
+import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
await dispatch(loadSearchResultsPanel());
});
+export const loadVirtualMachines = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
+ });
+
export const loadRepositories = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadRepositoriesPanel());
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core';
+import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
import { withDialog } from '~/store/dialog/with-dialog';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { connect } from 'react-redux';
-import { CurrentTokenDialogData, getCurrentTokenDialogData } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { CurrentTokenDialogData, getCurrentTokenDialogData, CURRENT_TOKEN_DIALOG_NAME } from '~/store/current-token-dialog/current-token-dialog-actions';
import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
type CssRules = 'link' | 'paper' | 'button';
export const CurrentTokenDialog =
withStyles(styles)(
connect(getCurrentTokenDialogData)(
- withDialog('currentTokenDialog')(
+ withDialog(CURRENT_TOKEN_DIALOG_NAME)(
class extends React.Component<CurrentTokenProps> {
render() {
const { classes, open, closeDialog, ...data } = this.props;
import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions';
import { openRepositoriesPanel } from "~/store/repositories/repositories-actions";
import { navigateToSshKeys } from '~/store/navigation/navigation-action';
+import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
interface AccountMenuProps {
user?: User;
<MenuItem>
{getUserFullname(user)}
</MenuItem>
+ <MenuItem onClick={() => dispatch(openVirtualMachines())}>Virtual Machines</MenuItem>
<MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute } from '~/routes/routes';
+import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from '~/routes/routes';
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
interface MainContentBarProps {
return !!match;
};
+const isVirtualMachinePath = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchVirtualMachineRoute(pathname);
+ return !!match;
+};
+
const isRepositoriesPath = ({ router }: RootState) => {
const pathname = router.location ? router.location.pathname : '';
const match = matchRepositoriesRoute(pathname);
};
export const MainContentBar = connect((state: RootState) => ({
- buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state)
+ buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state)
}), {
onDetailsPanelToggle: toggleDetailsPanel
})((props: MainContentBarProps) =>
import { ProjectResource } from "~/models/project";
import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
-import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { WorkflowIcon } from '~/components/icon/icon';
import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
import { noop } from 'lodash';
return FavoriteIcon;
case SidePanelTreeCategory.PROJECTS:
return ProjectsIcon;
- case SidePanelTreeCategory.RECENT_OPEN:
- return RecentIcon;
case SidePanelTreeCategory.SHARED_WITH_ME:
return ShareMeIcon;
case SidePanelTreeCategory.TRASH:
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as 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 { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
+import { Link } from 'react-router-dom';
+import { Dispatch, compose } from 'redux';
+import { saveRequestedDate, loadVirtualMachinesData } from '~/store/virtual-machines/virtual-machines-actions';
+import { RootState } from '~/store/store';
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { HelpIcon } from '~/components/icon/icon';
+import { VirtualMachinesLoginsResource, VirtualMachinesResource } from '~/models/virtual-machines';
+import { Routes } from '~/routes/routes';
+
+type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ marginTop: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
+ },
+ codeSnippet: {
+ borderRadius: theme.spacing.unit * 0.5,
+ border: '1px solid',
+ borderColor: theme.palette.grey["400"],
+ },
+ link: {
+ textDecoration: 'none',
+ color: theme.palette.primary.main,
+ "&:hover": {
+ color: theme.palette.primary.dark,
+ transition: 'all 0.5s ease'
+ }
+ },
+ linkIcon: {
+ textDecoration: 'none',
+ color: theme.palette.grey["500"],
+ textAlign: 'right',
+ "&:hover": {
+ color: theme.palette.common.black,
+ transition: 'all 0.5s ease'
+ }
+ },
+ rightAlign: {
+ textAlign: "right"
+ },
+ cardWithoutMachines: {
+ display: 'flex'
+ },
+ icon: {
+ textAlign: "right",
+ marginTop: theme.spacing.unit
+ }
+});
+
+const mapStateToProps = ({ virtualMachines }: RootState) => {
+ return {
+ requestedDate: virtualMachines.date,
+ ...virtualMachines
+ };
+};
+
+const mapDispatchToProps = {
+ saveRequestedDate,
+ loadVirtualMachinesData
+};
+
+interface VirtualMachinesPanelDataProps {
+ requestedDate: string;
+ virtualMachines: ListResults<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+interface VirtualMachinesPanelActionProps {
+ saveRequestedDate: () => void;
+ loadVirtualMachinesData: () => string;
+}
+
+type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
+
+export const VirtualMachinePanel = compose(
+ withStyles(styles),
+ connect(mapStateToProps, mapDispatchToProps))(
+ class extends React.Component<VirtualMachineProps> {
+ componentDidMount() {
+ this.props.loadVirtualMachinesData();
+ }
+
+ render() {
+ const { virtualMachines, links } = this.props;
+ return (
+ <Grid container spacing={16}>
+ {virtualMachines.itemsAvailable === 0 && <CardContentWithNoVirtualMachines {...this.props} />}
+ {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
+ {<CardSSHSection {...this.props} />}
+ </Grid>
+ );
+ }
+ }
+ );
+
+const CardContentWithNoVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent className={props.classes.cardWithoutMachines}>
+ <Grid item xs={6}>
+ <Typography variant="body2">
+ 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.
+ </Typography>
+ </Grid>
+ <Grid item xs={6} className={props.classes.rightAlign}>
+ <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {props.requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {props.requestedDate}
+ </Typography>}
+ </Grid>
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <div className={props.classes.rightAlign}>
+ <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {props.requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {props.requestedDate}
+ </Typography>}
+ </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}>
+ <Tooltip title="Access VM using webshell">
+ <HelpIcon />
+ </Tooltip>
+ </a>
+ </div>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <TableCell>Host name</TableCell>
+ <TableCell>Login name</TableCell>
+ <TableCell>Command line</TableCell>
+ <TableCell>Web shell</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {props.virtualMachines.items.map((it, index) =>
+ <TableRow key={index}>
+ <TableCell>{it.hostname}</TableCell>
+ <TableCell>{getUsername(props.links, it)}</TableCell>
+ <TableCell>ssh {getUsername(props.links, it)}@shell.arvados</TableCell>
+ <TableCell>
+ <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links, it)}`} target="_blank" className={props.classes.link}>
+ Log in as {getUsername(props.links, it)}
+ </a>
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const getUsername = (links: ListResults<any>, virtualMachine: VirtualMachinesResource) => {
+ const link = links.items.find((item: any) => item.headUuid === virtualMachine.uuid);
+ return link.properties.username || undefined;
+};
+
+const CardSSHSection = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <Typography variant="body2">
+ In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
+ </Typography>
+ <DefaultCodeSnippet
+ className={props.classes.codeSnippet}
+ lines={[textSSH]} />
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const textSSH = `Host *.arvados
+ TCPKeepAlive yes
+ ServerAliveInterval 60
+ ProxyCommand ssh -p2222 turnout@switchyard.api.ardev.roche.com -x -a $SSH_PROXY_FLAGS %h`;
\ No newline at end of file
import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog';
import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog';
import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog';
+import { VirtualMachinePanel } from '~/views/virtual-machine-panel/virtual-machine-panel';
import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog';
import { RepositoriesPanel } from '~/views/repositories-panel/repositories-panel';
import { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog';
<Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
<Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
<Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
+ <Route path={Routes.VIRTUAL_MACHINES} component={VirtualMachinePanel} />
<Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
<Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
</Switch>