// 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} />;
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
USER = "arvados#user",
+ VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
NONE = "arvados#none"
}
GROUP = 'j7d0g',
LOG = '57u5n',
USER = 'tpzed',
+ VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
}
return ResourceKind.LOG;
case ResourceObjectType.WORKFLOW:
return ResourceKind.WORKFLOW;
+ case ResourceObjectType.VIRTUAL_MACHINE:
+ return ResourceKind.VIRTUAL_MACHINE;
default:
return undefined;
}
--- /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 ViruatlMachinesLoginsResource {
+ 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 } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchVirtualMachineRoute } from './routes';
+import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, 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);
if (projectMatch) {
store.dispatch(loadWorkflow);
} else if (searchResultsMatch) {
store.dispatch(loadSearchResults);
+ } else if (virtualMachineMatch) {
+ store.dispatch(loadVirtualMachines);
}
};
PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
SHARED_WITH_ME: '/shared-with-me',
RUN_PROCESS: '/run-process',
+ VIRTUAL_MACHINES: '/virtual-machines',
WORKFLOWS: '/workflows',
SEARCH_RESULTS: '/search-results'
};
export const matchRunProcessRoute = (route: string) =>
matchPath(route, { path: Routes.RUN_PROCESS });
-
+
export const matchWorkflowRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.WORKFLOWS });
export const matchSearchResultsRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
+
+export const matchVirtualMachineRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
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";
export type ServiceRepository = ReturnType<typeof createServices>;
const permissionService = new PermissionService(apiClient, actions);
const projectService = new ProjectService(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 navigateToRunProcess = push(Routes.RUN_PROCESS);
export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
+
+export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
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";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
progressIndicator: progressIndicatorReducer,
runProcessPanel: runProcessPanelReducer,
appInfo: appInfoReducer,
- searchBar: searchBarReducer
+ searchBar: searchBarReducer,
+ virtualMachines: virtualMachinesReducer
});
--- /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";
+
+export const virtualMachinesAction = unionize({
+ SET_REQUESTED_DATE: ofType<string>(),
+});
+
+export type VirtualMachineActions = UnionOf<typeof virtualMachinesAction>;
+
+export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
+
+export const openVirtualMachines = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const virtualMachines = await services.virtualMachineService.list();
+ // const logins = await services.virtualMachineService.logins(virtualMachines.items[0].uuid);
+ // const getAllLogins = await services.virtualMachineService.getAllLogins();
+ console.log(virtualMachines);
+ // console.log(logins);
+ // console.log(getAllLogins);
+ dispatch<any>(navigateToVirtualMachines);
+ };
+
+export const loadRequestedDate = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const date = services.virtualMachineService.getRequestedDate();
+ dispatch(virtualMachinesAction.SET_REQUESTED_DATE(date));
+ };
+
+export const saveRequestedDate = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const date = formatDate((new Date).toISOString());
+ services.virtualMachineService.saveRequestedDate(date);
+ dispatch<any>(loadRequestedDate());
+ };
+
+const virtualMachinesActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
+
+export const loadVirtualMachinesPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(virtualMachinesActions.REQUEST_ITEMS());
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { virtualMachinesAction, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions';
+
+interface VirtualMachines {
+ date: string;
+}
+
+const initialState: VirtualMachines = {
+ date: ''
+};
+
+export const virtualMachinesReducer = (state = initialState, action: VirtualMachineActions): VirtualMachines =>
+ virtualMachinesAction.match(action, {
+ SET_REQUESTED_DATE: date => ({ ...state, date }),
+ default: () => state
+ });
import { projectPanelColumns } from '~/views/project-panel/project-panel';
import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
import { matchRootRoute } from '~/routes/routes';
-import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs, setBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
import { navigateToProject } from '../navigation/navigation-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { ServiceRepository } from '~/services/services';
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';
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' }]));
+ });
+
const finishLoadingProject = (project: GroupContentsResource | string) =>
async (dispatch: Dispatch<any>) => {
const uuid = typeof project === 'string' ? project : project.uuid;
// 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 { logout } from "~/store/auth/auth-action";
import { RootState } from "~/store/store";
import { openCurrentTokenDialog } from '../../store/current-token-dialog/current-token-dialog-actions';
+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(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
import { matchWorkflowRoute } from '~/routes/routes';
+import { matchVirtualMachineRoute } from '~/routes/routes';
interface MainContentBarProps {
onDetailsPanelToggle: () => void;
return !!match;
};
+const isVirtualMachinePath = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchVirtualMachineRoute(pathname);
+ return !!match;
+};
+
export const MainContentBar = connect((state: RootState) => ({
- buttonVisible: !isWorkflowPath(state)
+ buttonVisible: !isWorkflowPath(state) && !isVirtualMachinePath(state)
}), {
onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
})((props: MainContentBarProps) =>
<Breadcrumbs />
</Grid>
<Grid item>
- {props.buttonVisible ? <Tooltip title="Additional Info">
+ {props.buttonVisible && <Tooltip title="Additional Info">
<IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
<DetailsIcon />
</IconButton>
- </Tooltip> : null}
+ </Tooltip>}
</Grid>
</Grid>
</Toolbar>);
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 } 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 } from 'redux';
+import { saveRequestedDate, loadRequestedDate } from '~/store/virtual-machines/virtual-machines-actions';
+import { RootState } from '~/store/store';
+
+type CssRules = 'button' | 'codeSnippet' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ marginTop: theme.spacing.unit,
+ marginBottom: theme.spacing.unit * 2
+ },
+ codeSnippet: {
+ borderRadius: theme.spacing.unit * 0.5,
+ border: '1px solid',
+ borderColor: theme.palette.grey["400"],
+ maxHeight: '400px'
+ },
+ link: {
+ textDecoration: 'none',
+ color: theme.palette.primary.main
+ },
+});
+
+const mapStateToProps = (state: RootState) => {
+ return {
+ requestedDate: state.virtualMachines.date
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ saveRequestedDate: () => dispatch<any>(saveRequestedDate()),
+ loadRequestedDate: () => dispatch<any>(loadRequestedDate())
+});
+
+interface VirtualMachinesPanelDataProps {
+ requestedDate: string;
+}
+
+interface VirtualMachinesPanelActionProps {
+ saveRequestedDate: () => void;
+ loadRequestedDate: () => string;
+}
+
+type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
+
+export const VirtualMachinePanel = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(
+ class extends React.Component<VirtualMachineProps> {
+ componentDidMount() {
+ this.props.loadRequestedDate();
+ }
+
+ render() {
+ const { classes, saveRequestedDate, requestedDate } = this.props;
+ return (
+ <Grid container spacing={16}>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <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>
+ <Button variant="contained" color="primary" className={classes.button} onClick={saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {requestedDate}
+ </Typography>}
+ </CardContent>
+ </Card>
+ </Grid>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <Typography variant="body2">
+ In order to access virtual machines using SSH, <Link to='' className={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={classes.codeSnippet}
+ lines={[textSSH]} />
+ </CardContent>
+ </Card>
+ </Grid>
+ </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';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<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} />
</Switch>
</Grid>
</Grid>