Merge branch 'master' into 14393-vocabulary
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Thu, 29 Nov 2018 09:42:35 +0000 (10:42 +0100)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Thu, 29 Nov 2018 09:42:35 +0000 (10:42 +0100)
refs #14393

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

32 files changed:
src/components/icon/icon.tsx
src/index.tsx
src/models/link.ts
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/advanced-tab/advanced-tab.ts
src/store/current-token-dialog/current-token-dialog-actions.tsx
src/store/navigation/navigation-action.ts
src/store/search-results-panel/search-results-panel-actions.ts
src/store/sharing-dialog/sharing-dialog-actions.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/store/workflow-panel/workflow-panel-actions.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/ssh-key-action-set.ts
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/data-explorer/renderers.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/run-process-panel/run-process-panel.tsx
src/views/virtual-machine-panel/virtual-machine-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx
src/views/workflow-panel/workflow-panel-view.tsx
src/views/workflow-panel/workflow-panel.tsx

index a0f58be4e8df41dd0890a639a839f3d7c8d7a2ac..8049686f3d749c56db2bab6a55b99d63c975a65e 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';
@@ -90,7 +89,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 62db7f0face2f84d1fde544df6981c1c2a8d627f..801a56a1d382a3ac589af3bb732a085190567b1c 100644 (file)
@@ -306,3 +306,4 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) =>
     });
 };
 
+// force build comment #1
index 9d1711d8316c3e327d1f8e2edf1b59cd267a852c..baaff658a205f0cc5427e4a7ff796ebd93b64255 100644 (file)
@@ -3,13 +3,14 @@
 // 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 {
index 5fa617974b173741e69e3b017c7778223a606cc9..7e2127b2813dad8076881b8fc69a03bb82184633 100644 (file)
@@ -31,6 +31,7 @@ export enum ResourceKind {
     REPOSITORY = "arvados#repository",
     SSH_KEY = "arvados#authorizedKeys",
     USER = "arvados#user",
+    VIRTUAL_MACHINE = "arvados#virtualMachine",
     WORKFLOW = "arvados#workflow",
     NONE = "arvados#none"
 }
@@ -43,7 +44,9 @@ export enum ResourceObjectType {
     LOG = '57u5n',
     REPOSITORY = 's0uqq',
     USER = 'tpzed',
+    VIRTUAL_MACHINE = '2x53u',
     WORKFLOW = '7fd4e',
+    SSH_KEY = 'fngyi'
 }
 
 export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
@@ -76,8 +79,12 @@ export const extractUuidKind = (uuid: string = '') => {
             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:
+            return ResourceKind.SSH_KEY;
         default:
             return undefined;
     }
diff --git a/src/models/virtual-machines.ts b/src/models/virtual-machines.ts
new file mode 100644 (file)
index 0000000..0652c35
--- /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 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
index c7f3555bcf79db6f64448b330cb4132392a913cb..22d0b7c711364d2a466b8b0af5e2b7d465cb73aa 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, 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';
 
@@ -27,6 +27,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);
     const sshKeysMatch = matchSshKeysRoute(pathname);
 
@@ -52,6 +53,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadWorkflow);
     } else if (searchResultsMatch) {
         store.dispatch(loadSearchResults);
+    } else if (virtualMachineMatch) {
+        store.dispatch(loadVirtualMachines);
     } else if(repositoryMatch) {
         store.dispatch(loadRepositories);
     } else if (sshKeysMatch) {
index c9c2ae20e1eff73e55ae7d76b6e52aa3c2658f00..71cdfdacad218da9ea51cf636ffacd2cf95916ad 100644 (file)
@@ -19,6 +19,7 @@ export const Routes = {
     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`
@@ -79,6 +80,9 @@ export const matchWorkflowRoute = (route: string) =>
 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 });
     
index b0d3ba67c8b8b65017b69f293f23063a3ad450da..b24b1d99a181a8086e9da06bd41684cd5969a907 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";
 import { RepositoriesService } from '~/services/repositories-service/repositories-service';
 import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
 import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service';
@@ -48,6 +49,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
     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);
@@ -79,6 +81,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
         searchService,
         tagService,
         userService,
+        virtualMachineService,
         webdavClient,
         workflowService,
         vocabularyService,
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 c5f600d4d52a24ab9a90b43f63a7e30610ef0733..b3c5164c5561e8d3e8104b6a4d700e9c904a21df 100644 (file)
@@ -2,9 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { Dispatch } from 'redux';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { RootState } from '~/store/store';
-import { Dispatch } from 'redux';
 import { ResourceKind, extractUuidKind } from '~/models/resource';
 import { getResource } from '~/store/resources/resources';
 import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
@@ -15,10 +15,11 @@ import { ProjectResource } from '~/models/project';
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { RepositoryResource } from '~/models/repositories';
+import { SshKeyResource } from '~/models/ssh-key';
 
 export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
 
-export interface AdvancedTabDialogData {
+interface AdvancedTabDialogData {
     apiResponse: any;
     metadata: any;
     user: string;
@@ -52,40 +53,65 @@ enum RepositoryData {
     CREATED_AT = 'created_at'
 }
 
+enum SshKeyData {
+    SSH_KEY = 'authorized_keys',
+    CREATED_AT = 'created_at'
+}
+
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData;
+type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys';
+
 export const openAdvancedTabDialog = (uuid: string, index?: number) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
         const kind = extractUuidKind(uuid);
-        const data = getResource<any>(uuid)(resources);
-        const repositoryData = getState().repositories.items[index!];
-        if (data || repositoryData) {
-            if (data) {
-                const metadata = await services.linkService.list({
-                    filters: new FilterBuilder()
-                        .addEqual('headUuid', uuid)
-                        .getFilters()
-                });
-                const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid);
-                if (kind === ResourceKind.COLLECTION) {
-                    const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed);
-                    dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection }));
-                } else if (kind === ResourceKind.PROCESS) {
-                    const dataProcess: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, containerRequestApiResponse, data, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, data.outputName);
-                    dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProcess }));
-                } else if (kind === ResourceKind.PROJECT) {
-                    const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt);
-                    dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject }));
-                }
-            } else if (kind === ResourceKind.REPOSITORY) {
-                const dataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, repositoryData, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, repositoryData.createdAt);
-                dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataRepository }));
-            }
-        } else {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+        switch (kind) {
+            case ResourceKind.COLLECTION:
+                const { data: dataCollection, metadata: metaCollection, user: userCollection } = await dispatch<any>(getDataForAdvancedTab(uuid));
+                const advanceDataCollection: AdvancedTabDialogData = advancedTabData(uuid, metaCollection, userCollection, collectionApiResponse, dataCollection, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, dataCollection.storageClassesConfirmed);
+                dispatch<any>(initAdvancedTabDialog(advanceDataCollection));
+                break;
+            case ResourceKind.PROCESS:
+                const { data: dataProcess, metadata: metaProcess, user: userProcess } = await dispatch<any>(getDataForAdvancedTab(uuid));
+                const advancedDataProcess: AdvancedTabDialogData = advancedTabData(uuid, metaProcess, userProcess, containerRequestApiResponse, dataProcess, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, dataProcess.outputName);
+                dispatch<any>(initAdvancedTabDialog(advancedDataProcess));
+                break;
+            case ResourceKind.PROJECT:
+                const { data: dataProject, metadata: metaProject, user: userProject } = await dispatch<any>(getDataForAdvancedTab(uuid));
+                const advanceDataProject: AdvancedTabDialogData = advancedTabData(uuid, metaProject, userProject, groupRequestApiResponse, dataProject, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, dataProject.deleteAt);
+                dispatch<any>(initAdvancedTabDialog(advanceDataProject));
+                break;
+            case ResourceKind.REPOSITORY:
+                const dataRepository = getState().repositories.items[index!];
+                const advanceDataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, dataRepository, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, dataRepository.createdAt);
+                dispatch<any>(initAdvancedTabDialog(advanceDataRepository));
+                break;
+            case ResourceKind.SSH_KEY:
+                const dataSshKey = getState().auth.sshKeys[index!];
+                const advanceDataSshKey: AdvancedTabDialogData = advancedTabData(uuid, '', '', sshKeyApiResponse, dataSshKey, SshKeyData.SSH_KEY, 'authorized_keys', SshKeyData.CREATED_AT, dataSshKey.createdAt);
+                dispatch<any>(initAdvancedTabDialog(advanceDataSshKey));
+                break;
+            default:
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
     };
 
-const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: CollectionData | ProcessData | ProjectData | RepositoryData, resourcePrefix: GroupContentsResourcePrefix | 'repositories', resourceKindProperty: CollectionData | ProcessData | ProjectData | RepositoryData, property: any) => {
+const getDataForAdvancedTab = (uuid: string) =>
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const data = getResource<any>(uuid)(resources);
+        const metadata = await services.linkService.list({
+            filters: new FilterBuilder()
+                .addEqual('headUuid', uuid)
+                .getFilters()
+        });
+        const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid || '');
+        return { data, metadata, user };
+    };
+
+const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data });
+
+const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: AdvanceResourceKind, 
+    resourcePrefix: AdvanceResourcePrefix, resourceKindProperty: AdvanceResourceKind, property: any) => {
     return {
         uuid,
         user,
@@ -108,7 +134,7 @@ const pythonHeader = (resourceKind: string) =>
 const pythonExample = (uuid: string, resourcePrefix: string) => {
     const pythonExample = `import arvados
 
- x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`;
+x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`;
 
     return pythonExample;
 };
@@ -118,7 +144,7 @@ const cliGetHeader = (resourceKind: string) =>
 
 const cliGetExample = (uuid: string, resourceKind: string) => {
     const cliGetExample = `arv ${resourceKind} get \\
- --uuid ${uuid}`;
 --uuid ${uuid}`;
 
     return cliGetExample;
 };
@@ -127,9 +153,9 @@ const cliUpdateHeader = (resourceKind: string, resourceName: string) =>
     `An example arv command to update the "${resourceName}" attribute for the current ${resourceKind}:`;
 
 const cliUpdateExample = (uuid: string, resourceKind: string, resource: string | string[], resourceName: string) => {
-    const CLIUpdateCollectionExample = `arv ${resourceKind} update \\ 
- --uuid ${uuid} \\
- --${resourceKind} '{"${resourceName}":${resource}}'`;
+    const CLIUpdateCollectionExample = `arv ${resourceKind} update \\
 --uuid ${uuid} \\
 --${resourceKind} '{"${resourceName}":${resource}}'`;
 
     return CLIUpdateCollectionExample;
 };
@@ -139,10 +165,10 @@ const curlHeader = (resourceKind: string, resource: string) =>
 
 const curlExample = (uuid: string, resourcePrefix: string, resource: string | string[], resourceKind: string, resourceName: string) => {
     const curlExample = `curl -X PUT \\
- -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\
- --data-urlencode ${resourceKind}@/dev/stdin \\
- https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
- <<EOF
 -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\
 --data-urlencode ${resourceKind}@/dev/stdin \\
 https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
 <<EOF
 {
   "${resourceName}": ${resource}
 }
@@ -252,5 +278,19 @@ const repositoryApiResponse = (apiResponse: RepositoryResource) => {
 "name": ${stringify(name)},
 "created_at": "${createdAt}"`;
 
+    return response;
+};
+
+const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, authorizedUserUuid, expiresAt } = apiResponse;
+    const response = `"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"authorized_user_uuid": "${authorizedUserUuid}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"name": ${stringify(name)},
+"created_at": "${createdAt}",
+"expires_at": "${expiresAt}"`;
     return response;
 };
\ 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 fc08f3ac495403949d4aa2fc6f32a82e18db3bba..2bfd8b9944ec75e4911ab2bbd08c19676cab62c5 100644 (file)
@@ -62,6 +62,8 @@ export const navigateToRunProcess = push(Routes.RUN_PROCESS);
 
 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);
index 05da5b3e5787e36c1a9b97f128e11f4982994575..f7dc5d458f7fde7eb3af468253d4d816c5e5761f 100644 (file)
@@ -6,11 +6,13 @@ import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
 
 export const SEARCH_RESULTS_PANEL_ID = "searchResultsPanel";
 export const searchResultsPanelActions = bindDataExplorerActions(SEARCH_RESULTS_PANEL_ID);
 
 export const loadSearchResultsPanel = () =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([{ label: 'Search results' }]));
         dispatch(searchResultsPanelActions.REQUEST_ITEMS());
     };
\ No newline at end of file
index 0e3c76b28ef08f4c0c2a251d421cbc8e00d1aab4..37de6f8c2e9ab19e8a1c28c05ad4a20a8fb7013c 100644 (file)
@@ -56,13 +56,18 @@ export const sendSharingInvitations = async (dispatch: Dispatch) => {
 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
 
     const dialog = getDialog<string>(getState().dialog, SHARING_DIALOG_NAME);
-
     if (dialog) {
         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
-        const { items } = await permissionService.listResourcePermissions(dialog.data);
-        dispatch<any>(initializePublicAccessForm(items));
-        await dispatch<any>(initializeManagementForm(items));
-        dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+        try {
+            const { items } = await permissionService.listResourcePermissions(dialog.data);
+            dispatch<any>(initializePublicAccessForm(items));
+            await dispatch<any>(initializeManagementForm(items));
+            dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You do not have access to share this item', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+            dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
+            dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+        }
     }
 };
 
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 5e648c99ef9efadd32dffba96a493cd8dde2c397..4ab0918e60fb0ac6a636d03e4f95d0d9d3c6e468 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";
 import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
 
 const composeEnhancers =
@@ -113,5 +114,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     runProcessPanel: runProcessPanelReducer,
     appInfo: appInfoReducer,
     searchBar: searchBarReducer,
+    virtualMachines: virtualMachinesReducer,
     repositories: repositoriesReducer
 });
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..9bd7988
--- /dev/null
@@ -0,0 +1,65 @@
+// 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());
+    };
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..fa28417
--- /dev/null
@@ -0,0 +1,42 @@
+// 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
+    });
index 5e33661cfff12f9c0442de7f09011f3381e7a1a0..12dbe7b1a8a2d71ebfcd35eb4c2fb27cd8c6fdec 100644 (file)
@@ -54,6 +54,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';
 import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
@@ -392,6 +393,12 @@ export const loadSearchResults = handleFirstTimeLoad(
         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());
index da0da54ddb46e8de55d9d7743145b5a49fb29da9..3d51cbb8dbff61db38620569ff19d6087b78287a 100644 (file)
@@ -9,7 +9,9 @@ import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-act
 import { propertiesActions } from '~/store/properties/properties-actions';
 import { getResource } from '../resources/resources';
 import { getProperty } from '~/store/properties/properties';
-import { WorkflowResource } from '../../models/workflow';
+import { WorkflowResource } from '~/models/workflow';
+import { navigateToRunProcess } from '~/store/navigation/navigation-action';
+import { goToStep, runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
 
 export const WORKFLOW_PANEL_ID = "workflowPanel";
 const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
@@ -17,8 +19,10 @@ const WORKFLOW_PANEL_DETAILS_UUID = 'workflowPanelDetailsUuid';
 export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID);
 
 export const loadWorkflowPanel = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(workflowPanelActions.REQUEST_ITEMS());
+        const response = await services.workflowService.list();
+        dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
     };
 
 export const setUuidPrefix = (uuidPrefix: string) =>
@@ -28,6 +32,17 @@ export const getUuidPrefix = (state: RootState) => {
     return state.properties.uuidPrefix;
 };
 
+export const openRunProcess = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {  
+        const workflows = getState().runProcessPanel.searchWorkflows;
+        const workflow = workflows.find(workflow => workflow.uuid === uuid);
+        dispatch<any>(navigateToRunProcess);
+        dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL()); 
+        dispatch<any>(goToStep(1));
+        dispatch(runProcessPanelActions.SET_STEP_CHANGED(true));
+        dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow!));       
+    };
+
 export const getPublicUserUuid = (state: RootState) => {
     const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
     return `${prefix}-tpzed-anonymouspublic`;
index 5e1182bb0b3c1daa3f52d3a82bdc8fa0f1b6b548..8c81e3bd1996633ff39eca407e3547a6de0f3cf3 100644 (file)
@@ -60,13 +60,13 @@ export const projectActionSet: ContextMenuActionSet = [[
             dispatch<any>(openMoveProjectDialog(resource));
         }
     },
-    {
-        icon: CopyIcon,
-        name: "Copy to project",
-        execute: (dispatch, resource) => {
-            // add code
-        }
-    },
+    // {
+    //     icon: CopyIcon,
+    //     name: "Copy to project",
+    //     execute: (dispatch, resource) => {
+    //         // add code
+    //     }
+    // },
     {
         icon: DetailsIcon,
         name: "View details",
index 3fa2f16fa4c05401b002dcfa949e7ea20274bb7b..6e86b2bc17a2044d05e3ab3eb320e0cbc8fb0e0c 100644 (file)
@@ -5,6 +5,7 @@
 import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
 import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon";
 import { openSshKeyRemoveDialog, openSshKeyAttributesDialog } from '~/store/auth/auth-action';
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
 
 export const sshKeyActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -16,7 +17,7 @@ export const sshKeyActionSet: ContextMenuActionSet = [[{
     name: "Advanced",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid, index }) => {
-        // ToDo
+        dispatch<any>(openAdvancedTabDialog(uuid, index));
     }
 }, {
     name: "Remove",
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 87ba73ff0ef43b2fa9b8de60c69f0c47ad8727fd..a032b3ed5a22945e952d20767afd924fb931d4bd 100644 (file)
@@ -18,8 +18,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { compose, Dispatch } from 'redux';
 import { WorkflowResource } from '~/models/workflow';
 import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
-import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
-import { CollectionResource } from "~/models/collection";
+import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
 import { getResourceData } from "~/store/resources-data/resources-data";
 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
 
@@ -87,12 +86,11 @@ const getPublicUuid = (uuidPrefix: string) => {
     return `${uuidPrefix}-tpzed-anonymouspublic`;
 };
 
-// ToDo: share onClick
 export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
     const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
     return (
         <div>
-            { isPublic && uuid &&
+            {!isPublic && uuid &&
                 <Tooltip title="Share">
                     <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
                         <ShareIcon />
@@ -115,6 +113,28 @@ export const ResourceShare = connect(
     })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
         resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
 
+export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
+    return (
+        <div>
+            {uuid &&
+                <Tooltip title="Run process">
+                    <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
+                        <ProcessIcon />
+                    </IconButton>
+                </Tooltip>}
+        </div>
+    );
+};
+
+export const ResourceRunProcess = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+        return {
+            uuid: resource ? resource.uuid : ''
+        };
+    })((props: { uuid: string } & DispatchProp<any>) =>
+        resourceRunProcess(props.dispatch, props.uuid));
+
 export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
     if (ownerUuid === getPublicUuid(uuidPrefix)) {
         return renderStatus(ResourceStatus.PUBLIC);
index f00c678e15c573abe0572c6e729bafd887edff3d..ca88021ccdb04c6491ec5389f94dade7ccd35424 100644 (file)
@@ -13,6 +13,7 @@ import { RootState } from "~/store/store";
 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;
@@ -32,6 +33,7 @@ export const AccountMenu = connect(mapStateToProps)(
                 <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>
index 741a7e00f68481b07c24895cbcd5ecf05b8f8290..6b84bde2b6143a6e00abd80e6a17068eb27be66c 100644 (file)
@@ -8,7 +8,7 @@ import { DetailsIcon } from "~/components/icon/icon";
 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 {
@@ -22,6 +22,12 @@ const isWorkflowPath = ({ router }: RootState) => {
     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);
@@ -35,7 +41,7 @@ const isSshKeysPath = ({ router }: RootState) => {
 };
 
 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) =>
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:
index c8411ad719e68a05175512e5b5da08fff0c50b92..c5b95c3b07e2bcae58204ba7d559fe6e7c883a79 100644 (file)
@@ -6,7 +6,7 @@ import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
 import { RunProcessPanelRootDataProps, RunProcessPanelRootActionProps, RunProcessPanelRoot } from '~/views/run-process-panel/run-process-panel-root';
-import { goToStep, setWorkflow, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions';
+import { goToStep, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions';
 import { WorkflowResource } from '~/models/workflow';
 
 const mapStateToProps = ({ runProcessPanel }: RootState): RunProcessPanelRootDataProps => {
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..c94c3a7
--- /dev/null
@@ -0,0 +1,200 @@
+// 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
index 84c8e24c99dc0959ce9c34961d83d1e390478315..3914f64632e716bfe0aa4aca8265ae42a6a6bc93 100644 (file)
@@ -48,6 +48,7 @@ import { SshKeyPanel } from '~/views/ssh-key-panel/ssh-key-panel';
 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';
@@ -127,6 +128,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} />
                                 <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
                                 <Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
                             </Switch>
index b8e0e436d77612370a1eeb5f0c4ef4f2750f841a..da8a0c4bd303d5de5a66c86d6b4fb1adb8a6861c 100644 (file)
@@ -11,14 +11,15 @@ import {
     ResourceLastModifiedDate,
     RosurceWorkflowName,
     ResourceWorkflowStatus,
-    ResourceShare
+    ResourceShare,
+    ResourceRunProcess
 } from "~/views-components/data-explorer/renderers";
 import { SortDirection } from '~/components/data-table/data-column';
 import { DataColumns } from '~/components/data-table/data-table';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { Grid, Paper } from '@material-ui/core';
 import { WorkflowDetailsCard } from './workflow-description-card';
-import { WorkflowResource } from '../../models/workflow';
+import { WorkflowResource } from '~/models/workflow';
 import { createTree } from '~/models/tree';
 
 export enum WorkflowPanelColumnNames {
@@ -110,11 +111,18 @@ export const workflowPanelColumns: DataColumns<string> = [
         configurable: false,
         filters: createTree(),
         render: (uuid: string) => <ResourceShare uuid={uuid} />
+    },
+    {
+        name: '',
+        selected: true,
+        configurable: false,
+        filters: createTree(),
+        render: (uuid: string) => <ResourceRunProcess uuid={uuid} />
     }
 ];
 
 export const WorkflowPanelView = (props: WorkflowPanelProps) => {
-    return <Grid container spacing={16} style={{minHeight: '500px'}}>
+    return <Grid container spacing={16} style={{ minHeight: '500px' }}>
         <Grid item xs={6}>
             <DataExplorer
                 id={WORKFLOW_PANEL_ID}
index c59dc492f8b573dedcbfb63e0ca1c2ccfcb6cf86..99dc9a76889eb2166fc3c1d37b97495212dc2aae 100644 (file)
@@ -15,7 +15,6 @@ const mapStateToProps = (state: RootState): WorkflowPanelDataProps => ({
 });
 
 const mapDispatchToProps = (dispatch: Dispatch): WorfklowPanelActionProps => ({
-
     handleRowDoubleClick: (uuid: string) => {
         dispatch<any>(navigateTo(uuid));
     },