virtual-machines-panel-init
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 16 Nov 2018 12:00:21 +0000 (13:00 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 16 Nov 2018 12:00:21 +0000 (13:00 +0100)
Feature #13864

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

20 files changed:
src/components/icon/icon.tsx
src/models/resource.ts
src/models/virtual-machines.ts [new file with mode: 0644]
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/services.ts
src/services/virtual-machines-service/virtual-machines-service.ts [new file with mode: 0644]
src/store/current-token-dialog/current-token-dialog-actions.tsx
src/store/navigation/navigation-action.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/store.ts
src/store/virtual-machines/virtual-machines-actions.ts [new file with mode: 0644]
src/store/virtual-machines/virtual-machines-reducer.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/virtual-machine-panel/virtual-machine-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index a0fbd6ef970b46eb016d6e44599207be2c7c5bd8..4e1a0635704b533a358adc2e78718e3f575f1c05 100644 (file)
@@ -3,7 +3,6 @@
 // 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';
@@ -86,7 +85,6 @@ export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
 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} />;
index b8156cf2d12c0c7990e5f8aa8e869626f8bf17bc..f200713e6a6ceb4eea96ccec749c1c9534d79922 100644 (file)
@@ -29,6 +29,7 @@ export enum ResourceKind {
     PROCESS = "arvados#containerRequest",
     PROJECT = "arvados#group",
     USER = "arvados#user",
+    VIRTUAL_MACHINE = "arvados#virtualMachine",
     WORKFLOW = "arvados#workflow",
     NONE = "arvados#none"
 }
@@ -40,6 +41,7 @@ export enum ResourceObjectType {
     GROUP = 'j7d0g',
     LOG = '57u5n',
     USER = 'tpzed',
+    VIRTUAL_MACHINE = '2x53u',
     WORKFLOW = '7fd4e',
 }
 
@@ -73,6 +75,8 @@ export const extractUuidKind = (uuid: string = '') => {
             return ResourceKind.LOG;
         case ResourceObjectType.WORKFLOW:
             return ResourceKind.WORKFLOW;
+        case ResourceObjectType.VIRTUAL_MACHINE:
+            return ResourceKind.VIRTUAL_MACHINE;
         default:
             return undefined;
     }
diff --git a/src/models/virtual-machines.ts b/src/models/virtual-machines.ts
new file mode 100644 (file)
index 0000000..050e516
--- /dev/null
@@ -0,0 +1,18 @@
+// 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
index ef9e9ebcef15a01c271e7e6461b348f4e4b8237c..ca15a150aa626d8f3d1b3322448b093eb8ba60e2 100644 (file)
@@ -4,8 +4,8 @@
 
 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';
 
@@ -26,6 +26,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const searchResultsMatch = matchSearchResultsRoute(pathname);
     const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
     const runProcessMatch = matchRunProcessRoute(pathname);
+    const virtualMachineMatch = matchVirtualMachineRoute(pathname);
     const workflowMatch = matchWorkflowRoute(pathname);
 
     if (projectMatch) {
@@ -50,5 +51,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadWorkflow);
     } else if (searchResultsMatch) {
         store.dispatch(loadSearchResults);
+    } else if (virtualMachineMatch) {
+        store.dispatch(loadVirtualMachines);
     }
 };
index e5f3493539e3cbaae317e95fc1b38d3f89cb032f..3723847ca8b3ed5cc9632ae547e44f3f3602be91 100644 (file)
@@ -18,6 +18,7 @@ export const Routes = {
     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'
 };
@@ -70,9 +71,12 @@ export const matchSharedWithMeRoute = (route: string) =>
 
 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 });
index 5adf10b387891b0fecd1efddcfbdade21badb4e5..9e9fcc59d3d72201b26cb2f5614c7349b244997c 100644 (file)
@@ -24,6 +24,7 @@ import { ApiActions } from "~/services/api/api-actions";
 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>;
 
@@ -43,6 +44,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
     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);
@@ -71,6 +73,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
         searchService,
         tagService,
         userService,
+        virtualMachineService,
         webdavClient,
         workflowService,
     };
diff --git a/src/services/virtual-machines-service/virtual-machines-service.ts b/src/services/virtual-machines-service/virtual-machines-service.ts
new file mode 100644 (file)
index 0000000..c54eff4
--- /dev/null
@@ -0,0 +1,38 @@
+// 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
index 030b18e21835eaa94c14392c131560a3da93c481..fe8186b7c9c4eb21abb7006a68b09de64855863a 100644 (file)
@@ -3,7 +3,7 @@
 // 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';
 
index b63fc2cb290af38edb74be1da9e9beb28c3a5a2c..c4cf625271df1820e4742f92b3d92b31c005cce2 100644 (file)
@@ -61,3 +61,5 @@ export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
 export const navigateToRunProcess = push(Routes.RUN_PROCESS);
 
 export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
+
+export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
index 562f709614009d135b9a8be131986d625dc4be51..09009930d05062276548ab9f33f282d2340074b7 100644 (file)
@@ -20,7 +20,6 @@ export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
     SHARED_WITH_ME = 'Shared with me',
     WORKFLOWS = 'Workflows',
-    RECENT_OPEN = 'Recently open',
     FAVORITES = 'Favorites',
     TRASH = 'Trash'
 }
@@ -44,7 +43,6 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker)
 
 const SIDE_PANEL_CATEGORIES = [
     SidePanelTreeCategory.WORKFLOWS,
-    SidePanelTreeCategory.RECENT_OPEN,
     SidePanelTreeCategory.FAVORITES,
     SidePanelTreeCategory.TRASH,
 ];
index fa2a5be9bb658dedcf8a87829b209da72474b2b8..3f1f4a25b4ed4ce6e57b3ecdeb05117c45fe5915 100644 (file)
@@ -43,6 +43,7 @@ import { searchBarReducer } from './search-bar/search-bar-reducer';
 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' &&
@@ -111,5 +112,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     progressIndicator: progressIndicatorReducer,
     runProcessPanel: runProcessPanelReducer,
     appInfo: appInfoReducer,
-    searchBar: searchBarReducer
+    searchBar: searchBarReducer,
+    virtualMachines: virtualMachinesReducer
 });
diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts
new file mode 100644 (file)
index 0000000..09072a1
--- /dev/null
@@ -0,0 +1,50 @@
+// 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());
+    };
diff --git a/src/store/virtual-machines/virtual-machines-reducer.ts b/src/store/virtual-machines/virtual-machines-reducer.ts
new file mode 100644 (file)
index 0000000..26ba2a2
--- /dev/null
@@ -0,0 +1,19 @@
+// 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
+    });
index aaf8f2665f9a1b07318c6a9d3179a0fbd3727d5d..52d6c9e62f409aea7f3b7df1c7b007be7c2c887a 100644 (file)
@@ -14,7 +14,7 @@ import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-acti
 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';
@@ -53,6 +53,7 @@ import { collectionPanelActions } from "~/store/collection-panel/collection-pane
 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';
 
@@ -390,6 +391,12 @@ export const loadSearchResults = handleFirstTimeLoad(
         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;
index 503206a6f5be37d0c166d5b0ceaa3df7e14ec015..934be54d37f15336d5e6e5b5a89a66002bdf4b34 100644 (file)
@@ -3,12 +3,12 @@
 // 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';
@@ -36,7 +36,7 @@ type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyl
 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;
index fdd8123f280273a5e88984a368436a464f0eefac..baf893e2e77e7da55fe5eced464096bd574d8d36 100644 (file)
@@ -11,6 +11,7 @@ import { DispatchProp, connect } from 'react-redux';
 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;
@@ -30,6 +31,7 @@ export const AccountMenu = connect(mapStateToProps)(
                 <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>
index 6fb419e36710aa187e0f28a79b46fba82ed5f0d7..b38f85b5ff32cd44fe5aee2cd88934fcca93e753 100644 (file)
@@ -10,6 +10,7 @@ import { detailsPanelActions } from "~/store/details-panel/details-panel-action"
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
 import { matchWorkflowRoute } from '~/routes/routes';
+import { matchVirtualMachineRoute } from '~/routes/routes';
 
 interface MainContentBarProps {
     onDetailsPanelToggle: () => void;
@@ -22,8 +23,14 @@ const isWorkflowPath = ({ router }: RootState) => {
     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) =>
@@ -33,11 +40,11 @@ export const MainContentBar = connect((state: RootState) => ({
                     <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>);
index 33ee97f95fd7e99bc5b79ba93d23ef2f7f989b2e..dd5005c35557f1fdf769450fd79f2337a70f522b 100644 (file)
@@ -10,7 +10,7 @@ import { TreeItem } from "~/components/tree/tree";
 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';
@@ -59,8 +59,6 @@ const getSidePanelIcon = (category: string) => {
             return FavoriteIcon;
         case SidePanelTreeCategory.PROJECTS:
             return ProjectsIcon;
-        case SidePanelTreeCategory.RECENT_OPEN:
-            return RecentIcon;
         case SidePanelTreeCategory.SHARED_WITH_ME:
             return ShareMeIcon;
         case SidePanelTreeCategory.TRASH:
diff --git a/src/views/virtual-machine-panel/virtual-machine-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-panel.tsx
new file mode 100644 (file)
index 0000000..afe019b
--- /dev/null
@@ -0,0 +1,105 @@
+// 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
index 744526b1910117a2771a49ba24b6091dc2ea599e..4ebc99bd4754c94128643a6fee1336f3daf063dc 100644 (file)
@@ -47,6 +47,7 @@ import { SearchResultsPanel } from '~/views/search-results-panel/search-results-
 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';
 
@@ -116,6 +117,7 @@ export const WorkbenchPanel =
                                 <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>