Merge branch 'master' into 13864-Virtual-machines
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 26 Nov 2018 12:09:02 +0000 (13:09 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 26 Nov 2018 12:09:02 +0000 (13:09 +0100)
refs #13864

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

27 files changed:
src/components/icon/icon.tsx
src/index.tsx
src/models/resource.ts
src/store/advanced-tab/advanced-tab.ts
src/store/auth/auth-action.ts
src/store/auth/auth-actions.test.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/context-menu/context-menu-actions.ts
src/store/processes/process-copy-actions.ts
src/store/processes/process-move-actions.ts
src/store/processes/process-update-actions.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/sharing-dialog/sharing-dialog-actions.ts
src/views-components/advanced-tab-dialog/metadataTab.tsx
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/repository-action-set.ts
src/views-components/context-menu/action-sets/ssh-key-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/trashed-collection-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/ssh-keys-dialog/attributes-dialog.tsx [new file with mode: 0644]
src/views-components/ssh-keys-dialog/public-key-dialog.tsx [new file with mode: 0644]
src/views-components/ssh-keys-dialog/remove-dialog.tsx [new file with mode: 0644]
src/views/ssh-key-panel/ssh-key-panel-root.tsx
src/views/ssh-key-panel/ssh-key-panel.tsx
src/views/workbench/workbench.tsx

index f0f7da1d5f2694876095dee857d9509e080d7f53..8049686f3d749c56db2bab6a55b99d63c975a65e 100644 (file)
@@ -49,6 +49,7 @@ import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
 import Star from '@material-ui/icons/Star';
 import StarBorder from '@material-ui/icons/StarBorder';
 import Warning from '@material-ui/icons/Warning';
+import VpnKey from '@material-ui/icons/VpnKey';
 
 export type IconType = React.SFC<{ className?: string, style?: object }>;
 
@@ -73,6 +74,7 @@ export const HelpIcon: IconType = (props) => <Help {...props} />;
 export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
 export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
 export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
+export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
 export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
 export const MailIcon: IconType = (props) => <Mail {...props} />;
 export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
index 922720a411c29bf9eccd9b17b766605f8dc89f31..88fd2298bbc5a1936f23fb372e0220432b3c32f1 100644 (file)
@@ -49,6 +49,7 @@ import { DragDropContextProvider } from 'react-dnd';
 import HTML5Backend from 'react-dnd-html5-backend';
 import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions';
 import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set';
+import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set';
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -66,6 +67,7 @@ addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
+addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
 
 fetchConfig()
     .then(({ config, apiHost }) => {
index d2f524fc51d47a4537fd624fd47c9c9952e50b05..679a800a8b6d9e1ac5690c01b9adda0b414c806b 100644 (file)
@@ -29,6 +29,7 @@ export enum ResourceKind {
     PROCESS = "arvados#containerRequest",
     PROJECT = "arvados#group",
     REPOSITORY = "arvados#repository",
+    SSH_KEY = "arvados#authorizedKeys",
     USER = "arvados#user",
     VIRTUAL_MACHINE = "arvados#virtualMachine",
     WORKFLOW = "arvados#workflow",
index 6ad8af22a67ac8295d32793c276137fd2e6a4d98..c5f600d4d52a24ab9a90b43f63a7e30610ef0733 100644 (file)
@@ -21,7 +21,7 @@ export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
 export interface AdvancedTabDialogData {
     apiResponse: any;
     metadata: any;
-    uuid: string;
+    user: string;
     pythonHeader: string;
     pythonExample: string;
     cliGetHeader: string;
@@ -60,12 +60,12 @@ export const openAdvancedTabDialog = (uuid: string, index?: number) =>
         const repositoryData = getState().repositories.items[index!];
         if (data || repositoryData) {
             if (data) {
-                const user = await services.userService.get(data.ownerUuid);
                 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 }));
@@ -76,7 +76,6 @@ export const openAdvancedTabDialog = (uuid: string, index?: number) =>
                     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 }));
index 3658c589b6a4814f8dddc359494e744c1b0cf77c..28559b1a7ac126feac5adfb7c15170fa2f28295d 100644 (file)
@@ -4,7 +4,7 @@
 
 import { ofType, unionize, UnionOf } from '~/common/unionize';
 import { Dispatch } from "redux";
-import { reset, stopSubmit } from 'redux-form';
+import { reset, stopSubmit, startSubmit } from 'redux-form';
 import { AxiosInstance } from "axios";
 import { RootState } from "../store";
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
@@ -23,10 +23,14 @@ export const authActions = unionize({
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>(),
     SET_SSH_KEYS: ofType<SshKeyResource[]>(),
-    ADD_SSH_KEY: ofType<SshKeyResource>()
+    ADD_SSH_KEY: ofType<SshKeyResource>(),
+    REMOVE_SSH_KEY: ofType<string>()
 });
 
 export const SSH_KEY_CREATE_FORM_NAME = 'sshKeyCreateFormName';
+export const SSH_KEY_PUBLIC_KEY_DIALOG = 'sshKeyPublicKeyDialog';
+export const SSH_KEY_REMOVE_DIALOG = 'sshKeyRemoveDialog';
+export const SSH_KEY_ATTRIBUTES_DIALOG = 'sshKeyAttributesDialog';
 
 export interface SshKeyCreateFormDialogData {
     publicKey: string;
@@ -87,20 +91,51 @@ export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootSta
 
 export const openSshKeyCreateDialog = () => dialogActions.OPEN_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME, data: {} });
 
+export const openPublicKeyDialog = (name: string, publicKey: string) =>
+    dialogActions.OPEN_DIALOG({ id: SSH_KEY_PUBLIC_KEY_DIALOG, data: { name, publicKey } });
+
+export const openSshKeyAttributesDialog = (index: number) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const sshKey = getState().auth.sshKeys[index];
+        dispatch(dialogActions.OPEN_DIALOG({ id: SSH_KEY_ATTRIBUTES_DIALOG, data: { sshKey } }));
+    };
+
+export const openSshKeyRemoveDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: SSH_KEY_REMOVE_DIALOG,
+            data: {
+                title: 'Remove public key',
+                text: 'Are you sure you want to remove this public key?',
+                confirmButtonLabel: 'Remove',
+                uuid
+            }
+        }));
+    };
+
+export const removeSshKey = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+        await services.authorizedKeysService.delete(uuid);
+        dispatch(authActions.REMOVE_SSH_KEY(uuid));
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Public Key has been successfully removed.', hideDuration: 2000 }));
+    };
+
 export const createSshKey = (data: SshKeyCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = getState().auth.user!.uuid;
+        const { name, publicKey } = data;
+        dispatch(startSubmit(SSH_KEY_CREATE_FORM_NAME));
         try {
-            const userUuid = getState().auth.user!.uuid;
-            const { name, publicKey } = data;
             const newSshKey = await services.authorizedKeysService.create({
-                name, 
+                name,
                 publicKey,
                 keyType: KeyType.SSH,
                 authorizedUserUuid: userUuid
             });
+            dispatch(authActions.ADD_SSH_KEY(newSshKey));
             dispatch(dialogActions.CLOSE_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME }));
             dispatch(reset(SSH_KEY_CREATE_FORM_NAME));
-            dispatch(authActions.ADD_SSH_KEY(newSshKey));
             dispatch(snackbarActions.OPEN_SNACKBAR({
                 message: "Public key has been successfully created.",
                 hideDuration: 2000
index a1cd7f4f776776831956deae615b0ae0ed30878c..231c37b4effc6c2587bd4c1289c434cd7b0d1e83 100644 (file)
@@ -47,6 +47,7 @@ describe('auth-actions', () => {
 
         expect(store.getState().auth).toEqual({
             apiToken: "token",
+            sshKeys: [],
             user: {
                 email: "test@test.com",
                 firstName: "John",
index 25ce2c1122d7b9688eb71193ddb1be1ce3ec5649..8cde324549008b4790ff4d4595e4bfa46d55a948 100644 (file)
@@ -34,7 +34,8 @@ describe('auth-reducer', () => {
         const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
         expect(state).toEqual({
             apiToken: "token",
-            user
+            user,
+            sshKeys: []
         });
     });
 
index 8f234dad35bf8a9d68f508ab47d4a1166eea454e..a8e4340af52ac35142d9db3c73dce1c78de5fa7a 100644 (file)
@@ -10,7 +10,7 @@ import { SshKeyResource } from '~/models/ssh-key';
 export interface AuthState {
     user?: User;
     apiToken?: string;
-    sshKeys?: SshKeyResource[];
+    sshKeys: SshKeyResource[];
 }
 
 const initialState: AuthState = {
@@ -19,13 +19,13 @@ const initialState: AuthState = {
     sshKeys: []
 };
 
-export const authReducer = (services: ServiceRepository) => (state: AuthState = initialState, action: AuthAction) => {
+export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
     return authActions.match(action, {
         SAVE_API_TOKEN: (token: string) => {
             return {...state, apiToken: token};
         },
         INIT: ({ user, token }) => {
-            return { user, apiToken: token };
+            return { ...state, user, apiToken: token };
         },
         LOGIN: () => {
             return state;
@@ -40,7 +40,10 @@ export const authReducer = (services: ServiceRepository) => (state: AuthState =
             return {...state, sshKeys};
         },
         ADD_SSH_KEY: (sshKey: SshKeyResource) => {
-            return { ...state, sshKeys: state.sshKeys!.concat(sshKey) };
+            return { ...state, sshKeys: state.sshKeys.concat(sshKey) };
+        },
+        REMOVE_SSH_KEY: (uuid: string) => {
+            return { ...state, sshKeys: state.sshKeys.filter((sshKey) => sshKey.uuid !== uuid )};
         },
         default: () => state
     });
index 596ac87b098f89503ce248a9c82786a5f5f6ae78..5631a5e85cf9880b99fde0afd22f339a55ee734f 100644 (file)
@@ -14,6 +14,7 @@ import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree
 import { extractUuidKind, ResourceKind } from '~/models/resource';
 import { Process } from '~/store/processes/process';
 import { RepositoryResource } from '~/models/repositories';
+import { SshKeyResource } from '~/models/ssh-key';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -73,6 +74,18 @@ export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>,
             }));
     };
 
+export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, index: number, sshKey: SshKeyResource) =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(openContextMenu(event, {
+            name: '',
+            uuid: sshKey.uuid,
+            ownerUuid: sshKey.ownerUuid,
+            kind: ResourceKind.SSH_KEY,
+            menuKind: ContextMenuKind.SSH_KEY,
+            index
+        }));
+    };
+
 export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<UserResource>(projectUuid)(getState().resources);
@@ -119,10 +132,10 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
     (dispatch: Dispatch, getState: () => RootState) => {
         const resource = {
             uuid: process.containerRequest.uuid,
-            ownerUuid: '',
+            ownerUuid: process.containerRequest.ownerUuid,
             kind: ResourceKind.PROCESS,
-            name: '',
-            description: '',
+            name: process.containerRequest.name,
+            description: process.containerRequest.description,
             menuKind: ContextMenuKind.PROCESS
         };
         dispatch<any>(openContextMenu(event, resource));
index cd3fe21c28abb97d96334aa2c562202ba8202560..01387852107c3224bb6c348aca9b05189d4441c4 100644 (file)
@@ -9,7 +9,7 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
-import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
+import { getProcess } from '~/store/processes/process';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
@@ -19,16 +19,11 @@ export const openCopyProcessDialog = (resource: { name: string, uuid: string })
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const process = getProcess(resource.uuid)(getState().resources);
         if (process) {
-            const processStatus = getProcessStatus(process);
-            if (processStatus === ProcessStatus.DRAFT) {
-                dispatch<any>(resetPickerProjectTree());
-                dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
-                const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
-                dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
-                dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
-            } else {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can copy only draft processes.', hideDuration: 2000 }));
-            }
+            dispatch<any>(resetPickerProjectTree());
+            dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
+            const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
+            dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
         } else {
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
         }
@@ -39,9 +34,8 @@ export const copyProcess = (resource: CopyFormDialogData) =>
         dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
         try {
             const process = await services.containerRequestService.get(resource.uuid);
-            const uuidKey = 'uuid';
-            delete process[uuidKey];
-            await services.containerRequestService.create({ ...process, ownerUuid: resource.ownerUuid, name: resource.name });
+            const { kind, containerImage, outputPath, outputName, containerCountMax, command, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters } = process;
+            await services.containerRequestService.create({ command, containerImage, outputPath, ownerUuid: resource.ownerUuid, name: resource.name, kind, outputName, containerCountMax, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters });
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
             return process;
         } catch (e) {
index edba5a8574e16814c6a23988d85a1d4657d431b5..7e65bcca0bd221c8eea7bd293b77f42895ee26f5 100644 (file)
@@ -12,7 +12,7 @@ import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+import { getProcess } from '~/store/processes/process';
 import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
@@ -21,15 +21,10 @@ export const openMoveProcessDialog = (resource: { name: string, uuid: string })
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const process = getProcess(resource.uuid)(getState().resources);
         if (process) {
-            const processStatus = getProcessStatus(process);
-            if (processStatus === ProcessStatus.DRAFT) {
-                dispatch<any>(resetPickerProjectTree());
-                dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
-                dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
-                dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
-            } else {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can move only draft processes.', hideDuration: 2000 }));
-            }
+            dispatch<any>(resetPickerProjectTree());
+            dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
+            dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
         } else {
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
         }
@@ -40,7 +35,7 @@ export const moveProcess = (resource: MoveToFormDialogData) =>
         dispatch(startSubmit(PROCESS_MOVE_FORM_NAME));
         try {
             const process = await services.containerRequestService.get(resource.uuid);
-            await services.containerRequestService.update(resource.uuid, { ...process, ownerUuid: resource.ownerUuid });
+            await services.containerRequestService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
             return process;
@@ -48,8 +43,6 @@ export const moveProcess = (resource: MoveToFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' }));
-            } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
-                dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'You can move only draft processes.' }));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000 }));
index 92cf032f37ae57562960d61074802bf6c35d23a6..2063f11362ced7bb2b04e0327dc3743432bc9b48 100644 (file)
@@ -34,8 +34,7 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
         try {
-            const process = await services.containerRequestService.get(resource.uuid);
-            const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name });
+            const updatedProcess = await services.containerRequestService.update(resource.uuid, { name: resource.name });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
             return updatedProcess;
@@ -43,8 +42,6 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' }));
-            } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
-                dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'You cannot modified in "Final" state.' }));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 }));
index a21f7c04bf90b9b55462dc57e7eb2c0e31a48e1a..f1d2d2fd55aab72f72e03907f9e2460157cb4720 100644 (file)
@@ -17,6 +17,7 @@ import { navigateToProcess } from '../navigation/navigation-action';
 import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from '~/views/run-process-panel/run-process-advanced-form';
 import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
 import { dialogActions } from '~/store/dialog/dialog-actions';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
 
 export const runProcessPanelActions = unionize({
     SET_PROCESS_OWNER_UUID: ofType<string>(),
@@ -41,6 +42,7 @@ export type RunProcessPanelAction = UnionOf<typeof runProcessPanelActions>;
 export const loadRunProcessPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
+            dispatch(setBreadcrumbs([{ label: 'Run Process' }]));
             dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
             const response = await services.workflowService.list();
             dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
index d1ac14cb4a411d8c95f837841a74248ce92946ab..0e3c76b28ef08f4c0c2a251d421cbc8e00d1aab4 100644 (file)
@@ -50,6 +50,7 @@ export const sendSharingInvitations = async (dispatch: Dispatch) => {
         message: 'Resource has been shared',
         kind: SnackbarKind.SUCCESS,
     }));
+    dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
 };
 
 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
index 6250a7ad643c5cd48aba090a26a36312586f5572..bcf277c0b1d963d238e63e6fc2a14085ce676397 100644 (file)
@@ -47,7 +47,7 @@ export const MetadataTab = withStyles(styles)((props: MetadataProps & WithStyles
                     <TableCell className={props.classes.cell}>{it.uuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
                     <TableCell className={props.classes.cell}>{it.name}</TableCell>
-                    <TableCell className={props.classes.cell}>{props.user ? `User: ${props.user.firstName} ${props.user.lastName}` : it.tailUuid}</TableCell>
+                    <TableCell className={props.classes.cell}>{props.user && `User: ${props.user.firstName} ${props.user.lastName}`}</TableCell>
                     <TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
                 </TableRow>
index 9b8ced5663037596646b1d1598ebd8eb6bc74d80..5e1182bb0b3c1daa3f52d3a82bdc8fa0f1b6b548 100644 (file)
@@ -12,7 +12,6 @@ import { openProjectCreateDialog } from '~/store/projects/project-create-actions
 import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
 import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
 import { toggleProjectTrashed } from "~/store/trash/trash-actions";
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
 import { ShareIcon } from '~/components/icon/icon';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
index cf7fb883cfeb0bab89c0364085627e1ec2ceb1e9..22f6bee135c71f6b11fb6faf21b4af1fe27ed4f6 100644 (file)
@@ -23,8 +23,8 @@ export const repositoryActionSet: ContextMenuActionSet = [[{
 }, {
     name: "Advanced",
     icon: AdvancedIcon,
-    execute: (dispatch, { uuid, index }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid, index));
+    execute: (dispatch, resource) => {
+        dispatch<any>(openAdvancedTabDialog(resource.uuid, resource.index));
     }
 }, {
     name: "Remove",
diff --git a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts
new file mode 100644 (file)
index 0000000..3fa2f16
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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';
+
+export const sshKeyActionSet: ContextMenuActionSet = [[{
+    name: "Attributes",
+    icon: AttributesIcon,
+    execute: (dispatch, { index }) => {
+        dispatch<any>(openSshKeyAttributesDialog(index!));
+    }
+}, {
+    name: "Advanced",
+    icon: AdvancedIcon,
+    execute: (dispatch, { uuid, index }) => {
+        // ToDo
+    }
+}, {
+    name: "Remove",
+    icon: RemoveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openSshKeyRemoveDialog(uuid));
+    }
+}]];
index cefef345f0a0df2b842a764a0ae37ce0b1a5d25e..ea0d1aaf6fdb32d0a9063e3d19a14e0108767a49 100644 (file)
@@ -5,7 +5,6 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RestoreFromTrashIcon } from '~/components/icon/icon';
 import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
index 30ecc9810eb40d338c2ec37d538fb17d549cebea..af5aaa929c43f14623c9f8c6e85d43ddeb4ebf7f 100644 (file)
@@ -69,5 +69,6 @@ export enum ContextMenuKind {
     PROCESS = "Process",
     PROCESS_RESOURCE = 'ProcessResource',
     PROCESS_LOGS = "ProcessLogs",
-    REPOSITORY = "Repository"
+    REPOSITORY = "Repository",
+    SSH_KEY = "SshKey"
 }
index 6e25508d701c212e95343cfefceb31db70a402e5..87ba73ff0ef43b2fa9b8de60c69f0c47ad8727fd 100644 (file)
@@ -9,18 +9,19 @@ import { ResourceKind, TrashableResource } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
-import { connect } from 'react-redux';
+import { connect, DispatchProp } from 'react-redux';
 import { RootState } from '~/store/store';
 import { getResource } from '~/store/resources/resources';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import { getProcess, Process, getProcessStatus, getProcessStatusColor } from '~/store/processes/process';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { compose } from 'redux';
+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 { getResourceData } from "~/store/resources-data/resources-data";
+import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
 
 export const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -86,13 +87,20 @@ const getPublicUuid = (uuidPrefix: string) => {
     return `${uuidPrefix}-tpzed-anonymouspublic`;
 };
 
-// do share onClick
-export const resourceShare = (uuidPrefix: string, ownerUuid?: string) => {
-    return <Tooltip title="Share">
-        <IconButton onClick={() => undefined}>
-            {ownerUuid === getPublicUuid(uuidPrefix) ? <ShareIcon /> : null}
-        </IconButton>
-    </Tooltip>;
+// ToDo: share onClick
+export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
+    const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
+    return (
+        <div>
+            { isPublic && uuid &&
+                <Tooltip title="Share">
+                    <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
+                        <ShareIcon />
+                    </IconButton>
+                </Tooltip>
+            }
+        </div>
+    );
 };
 
 export const ResourceShare = connect(
@@ -100,10 +108,12 @@ export const ResourceShare = connect(
         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
         const uuidPrefix = getUuidPrefix(state);
         return {
+            uuid: resource ? resource.uuid : '',
             ownerUuid: resource ? resource.ownerUuid : '',
             uuidPrefix
         };
-    })((props: { ownerUuid?: string, uuidPrefix: string }) => resourceShare(props.uuidPrefix, props.ownerUuid));
+    })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
+        resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
 
 export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
     if (ownerUuid === getPublicUuid(uuidPrefix)) {
diff --git a/src/views-components/ssh-keys-dialog/attributes-dialog.tsx b/src/views-components/ssh-keys-dialog/attributes-dialog.tsx
new file mode 100644 (file)
index 0000000..ce896dc
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { compose } from 'redux';
+import { withStyles, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, WithStyles, Typography, Grid } from '@material-ui/core';
+import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
+import { SSH_KEY_ATTRIBUTES_DIALOG } from '~/store/auth/auth-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { SshKeyResource } from "~/models/ssh-key";
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        fontSize: '0.875rem',
+        '& div:nth-child(odd)': {
+            textAlign: 'right',
+            color: theme.palette.grey["500"]
+        }
+    }
+});
+
+interface AttributesSshKeyDialogDataProps {
+    sshKey: SshKeyResource;
+}
+
+export const AttributesSshKeyDialog = compose(
+    withDialog(SSH_KEY_ATTRIBUTES_DIALOG),
+    withStyles(styles))(
+        ({ open, closeDialog, data, classes }: WithDialogProps<AttributesSshKeyDialogDataProps> & WithStyles<CssRules>) =>
+            <Dialog open={open}
+                onClose={closeDialog}
+                fullWidth
+                maxWidth='sm'>
+                <DialogTitle>Attributes</DialogTitle>
+                <DialogContent>
+                    {data.sshKey && <Grid container direction="row" spacing={16} className={classes.root}>
+                        <Grid item xs={5}>Name</Grid>
+                        <Grid item xs={7}>{data.sshKey.name}</Grid>
+                        <Grid item xs={5}>uuid</Grid>
+                        <Grid item xs={7}>{data.sshKey.uuid}</Grid>
+                        <Grid item xs={5}>Owner uuid</Grid>
+                        <Grid item xs={7}>{data.sshKey.ownerUuid}</Grid>
+                        <Grid item xs={5}>Authorized user uuid</Grid>
+                        <Grid item xs={7}>{data.sshKey.authorizedUserUuid}</Grid>
+                        <Grid item xs={5}>Created at</Grid>
+                        <Grid item xs={7}>{data.sshKey.createdAt}</Grid>
+                        <Grid item xs={5}>Modified at</Grid>
+                        <Grid item xs={7}>{data.sshKey.modifiedAt}</Grid>
+                        <Grid item xs={5}>Expires at</Grid>
+                        <Grid item xs={7}>{data.sshKey.expiresAt}</Grid>
+                        <Grid item xs={5}>Modified by user uuid</Grid>
+                        <Grid item xs={7}>{data.sshKey.modifiedByUserUuid}</Grid>
+                        <Grid item xs={5}>Modified by client uuid</Grid>
+                        <Grid item xs={7}>{data.sshKey.modifiedByClientUuid}</Grid>
+                    </Grid>}
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={closeDialog}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </Dialog>
+    );
\ No newline at end of file
diff --git a/src/views-components/ssh-keys-dialog/public-key-dialog.tsx b/src/views-components/ssh-keys-dialog/public-key-dialog.tsx
new file mode 100644 (file)
index 0000000..77c6cfd
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { compose } from 'redux';
+import { withStyles, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
+import { SSH_KEY_PUBLIC_KEY_DIALOG } from '~/store/auth/auth-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
+
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    codeSnippet: {
+        borderRadius: theme.spacing.unit * 0.5,
+        border: '1px solid',
+        borderColor: theme.palette.grey["400"],
+        '& pre': {
+            wordWrap: 'break-word',
+            whiteSpace: 'pre-wrap'
+        }
+    },
+});
+
+interface PublicKeyDialogDataProps {
+    name: string;
+    publicKey: string;
+}
+
+export const PublicKeyDialog = compose(
+    withDialog(SSH_KEY_PUBLIC_KEY_DIALOG),
+    withStyles(styles))(
+        ({ open, closeDialog, data, classes }: WithDialogProps<PublicKeyDialogDataProps> & WithStyles<CssRules>) =>
+            <Dialog open={open}
+                onClose={closeDialog}
+                fullWidth
+                maxWidth='sm'>
+                <DialogTitle>{data.name} - SSH Key</DialogTitle>
+                <DialogContent>
+                    {data && data.publicKey && <DefaultCodeSnippet
+                        className={classes.codeSnippet}
+                        lines={data.publicKey.split(' ')} />}
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={closeDialog}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </Dialog>
+    );
\ No newline at end of file
diff --git a/src/views-components/ssh-keys-dialog/remove-dialog.tsx b/src/views-components/ssh-keys-dialog/remove-dialog.tsx
new file mode 100644 (file)
index 0000000..8077f21
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { SSH_KEY_REMOVE_DIALOG, removeSshKey } from '~/store/auth/auth-action';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(removeSshKey(props.data.uuid));
+    }
+});
+
+export const RemoveSshKeyDialog = compose(
+    withDialog(SSH_KEY_REMOVE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
index f752228ffc13452ac5f434172d50e60db472b977..869662dd0150f07e8fe07e11a6f0f2fcebe4c71e 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { SshKeyResource } from '~/models/ssh-key';
+import { AddIcon, MoreOptionsIcon, KeyIcon } from '~/components/icon/icon';
 
-
-type CssRules = 'root' | 'link';
+type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' | 'keyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-       width: '100%'
+       width: '100%',
+       overflow: 'auto'
     },
     link: {
         color: theme.palette.primary.main,
         textDecoration: 'none',
         margin: '0px 4px'
+    },
+    buttonContainer: {
+        textAlign: 'right'
+    },
+    table: {
+        marginTop: theme.spacing.unit
+    },
+    tableRow: {
+        '& td, th': {
+            whiteSpace: 'nowrap'
+        }
+    },
+    keyIcon: {
+        color: theme.palette.primary.main
     }
 });
 
 export interface SshKeyPanelRootActionProps {
-    onClick: () => void;
+    openSshKeyCreateDialog: () => void;
+    openRowOptions: (event: React.MouseEvent<HTMLElement>, index: number, sshKey: SshKeyResource) => void;
+    openPublicKeyDialog: (name: string, publicKey: string) => void;
 }
 
 export interface SshKeyPanelRootDataProps {
-    sshKeys?: SshKeyResource[];
+    sshKeys: SshKeyResource[];
+    hasKeys: boolean;
 }
 
 type SshKeyPanelRootProps = SshKeyPanelRootDataProps & SshKeyPanelRootActionProps & WithStyles<CssRules>;
 
 export const SshKeyPanelRoot = withStyles(styles)(
-    ({ classes, sshKeys, onClick }: SshKeyPanelRootProps) =>
+    ({ classes, sshKeys, openSshKeyCreateDialog, openPublicKeyDialog, hasKeys, openRowOptions }: SshKeyPanelRootProps) =>
         <Card className={classes.root}>
             <CardContent>
-                <Typography variant='body1' paragraph={true}>
-                    You have not yet set up an SSH public key for use with Arvados.
-                    <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html' target='blank' className={classes.link}>
-                        Learn more.
-                    </a>
-                </Typography>
-                <Typography variant='body1' paragraph={true}>
-                    When you have an SSH key you would like to use, add it using button below.
-                </Typography>
-                <Button
-                    onClick={onClick}
-                    color="primary"
-                    variant="contained">
-                    Add New Ssh Key
-                </Button>
+                <Grid container direction="row">
+                    <Grid item xs={8}>
+                        { !hasKeys && <Typography variant='body1' paragraph={true} >
+                            You have not yet set up an SSH public key for use with Arvados.
+                            <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html'
+                                target='blank' className={classes.link}>
+                                Learn more.
+                            </a>
+                        </Typography>}
+                        { !hasKeys && <Typography variant='body1' paragraph={true}>
+                            When you have an SSH key you would like to use, add it using button below.
+                        </Typography> }
+                    </Grid>
+                    <Grid item xs={4} className={classes.buttonContainer}>
+                        <Button onClick={openSshKeyCreateDialog} color="primary" variant="contained">
+                            <AddIcon /> Add New Ssh Key
+                        </Button>
+                    </Grid>
+                </Grid>
+                <Grid item xs={12}>
+                    {hasKeys && <Table className={classes.table}>
+                        <TableHead>
+                            <TableRow className={classes.tableRow}>
+                                <TableCell>Name</TableCell>
+                                <TableCell>UUID</TableCell>
+                                <TableCell>Authorized user</TableCell>
+                                <TableCell>Expires at</TableCell>
+                                <TableCell>Key type</TableCell>
+                                <TableCell>Public Key</TableCell>
+                                <TableCell />
+                            </TableRow>
+                        </TableHead>
+                        <TableBody>
+                            {sshKeys.map((sshKey, index) =>
+                                <TableRow key={index} className={classes.tableRow}>
+                                    <TableCell>{sshKey.name}</TableCell>
+                                    <TableCell>{sshKey.uuid}</TableCell>
+                                    <TableCell>{sshKey.authorizedUserUuid}</TableCell>
+                                    <TableCell>{sshKey.expiresAt || '(none)'}</TableCell>
+                                    <TableCell>{sshKey.keyType}</TableCell>
+                                    <TableCell>
+                                        <Tooltip title="Public Key" disableFocusListener>
+                                            <IconButton onClick={() => openPublicKeyDialog(sshKey.name, sshKey.publicKey)}>
+                                                <KeyIcon className={classes.keyIcon} />
+                                            </IconButton>
+                                        </Tooltip>
+                                    </TableCell>
+                                    <TableCell>
+                                        <Tooltip title="More options" disableFocusListener>
+                                            <IconButton onClick={event => openRowOptions(event, index, sshKey)}>
+                                                <MoreOptionsIcon />
+                                            </IconButton>
+                                        </Tooltip>
+                                    </TableCell>
+                                </TableRow>)}
+                        </TableBody>
+                    </Table>}
+                </Grid>
             </CardContent>
         </Card>
     );
\ No newline at end of file
index f600677d1bc152ffc1649723b017f658ee7beeae..c7e3516e0ab9cf2bede1b08df4256ab8c2821d55 100644 (file)
@@ -5,18 +5,26 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
+import { openSshKeyCreateDialog, openPublicKeyDialog } from '~/store/auth/auth-action';
+import { openSshKeyContextMenu } from '~/store/context-menu/context-menu-actions';
 import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps } from '~/views/ssh-key-panel/ssh-key-panel-root';
-import { openSshKeyCreateDialog } from '~/store/auth/auth-action';
 
 const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
     return {
-        sshKeys: state.auth.sshKeys
+        sshKeys: state.auth.sshKeys,
+        hasKeys: state.auth.sshKeys!.length > 0
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): SshKeyPanelRootActionProps => ({
-    onClick: () => {
-        dispatch(openSshKeyCreateDialog());
+    openSshKeyCreateDialog: () => {
+        dispatch<any>(openSshKeyCreateDialog());
+    },
+    openRowOptions: (event, index, sshKey) => {
+        dispatch<any>(openSshKeyContextMenu(event, index, sshKey));
+    },
+    openPublicKeyDialog: (name: string, publicKey: string) => {
+        dispatch<any>(openPublicKeyDialog(name, publicKey));
     }
 });
 
index 5ebf10567f1d87b1d0edc52343659f0cf645bedf..3914f64632e716bfe0aa4aca8265ae42a6a6bc93 100644 (file)
@@ -56,6 +56,9 @@ import { RepositoryAttributesDialog } from '~/views-components/repository-attrib
 import { CreateRepositoryDialog } from '~/views-components/dialog-forms/create-repository-dialog';
 import { RemoveRepositoryDialog } from '~/views-components/repository-remove-dialog/repository-remove-dialog';
 import { CreateSshKeyDialog } from '~/views-components/dialog-forms/create-ssh-key-dialog';
+import { PublicKeyDialog } from '~/views-components/ssh-keys-dialog/public-key-dialog';
+import { RemoveSshKeyDialog } from '~/views-components/ssh-keys-dialog/remove-dialog';
+import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -137,6 +140,7 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AdvancedTabDialog />
+            <AttributesSshKeyDialog />
             <ChangeWorkflowDialog />
             <ContextMenu />
             <CopyCollectionDialog />
@@ -152,12 +156,14 @@ export const WorkbenchPanel =
             <MoveProcessDialog />
             <MoveProjectDialog />
             <MultipleFilesRemoveDialog />
+            <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <ProcessCommandDialog />
             <ProcessInputDialog />
             <ProjectPropertiesDialog />
             <RemoveProcessDialog />
             <RemoveRepositoryDialog />
+            <RemoveSshKeyDialog />
             <RenameFileDialog />
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />