Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 12 Sep 2018 07:47:15 +0000 (09:47 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 12 Sep 2018 07:47:15 +0000 (09:47 +0200)
Feature #13751

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

38 files changed:
package.json
src/components/code-snippet/code-snippet.tsx
src/components/default-code-snippet/default-code-snippet.tsx
src/index.tsx
src/routes/routes.ts
src/services/common-service/common-resource-service.ts
src/store/collections/collection-copy-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/copy-dialog/copy-dialog.ts [new file with mode: 0644]
src/store/processes/process-command-actions.ts [new file with mode: 0644]
src/store/processes/process-copy-actions.ts [new file with mode: 0644]
src/store/processes/process-move-actions.ts [new file with mode: 0644]
src/store/processes/process-update-actions.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/process-action-set.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
src/views-components/dialog-copy/dialog-copy.tsx [moved from src/views-components/dialog-copy/dialog-collection-copy.tsx with 78% similarity]
src/views-components/dialog-forms/copy-collection-dialog.ts
src/views-components/dialog-forms/copy-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-forms/move-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-forms/update-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-update/dialog-process-update.tsx [new file with mode: 0644]
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/form-fields/process-form-fields.tsx [new file with mode: 0644]
src/views-components/process-command-dialog/process-command-dialog.tsx [new file with mode: 0644]
src/views/process-log-panel/process-log-code-snippet.tsx
src/views/process-log-panel/process-log-main-card.tsx
src/views/process-log-panel/process-log-panel-root.tsx
src/views/process-log-panel/process-log-panel.tsx
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/process-panel/process-subprocesses.tsx
src/views/workbench/workbench.tsx
yarn.lock

index 0e6435ebf536da29d695f224423f56780f4fca40..84d1510f5660226cf8a1f7abac8a1ca20a2c0509 100644 (file)
@@ -21,6 +21,7 @@
     "react-router-dom": "4.3.1",
     "react-router-redux": "5.0.0-alpha.9",
     "react-scripts-ts": "2.17.0",
+    "react-transition-group": "2.4.0",
     "redux": "4.0.0",
     "redux-thunk": "2.3.0",
     "unionize": "2.1.2"
index eb0e709a9b0dfa572860e15b796b436e065b41e3..6cba299f1580a70d8076afc98606af1479ab5fb1 100644 (file)
@@ -5,15 +5,13 @@
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, Typography, withStyles, Theme } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
+import * as classNames from 'classnames';
 
 type CssRules = 'root';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         boxSizing: 'border-box',
-        width: '100%',
-        height: 'auto',
-        maxHeight: '550px',
         overflow: 'auto',
         padding: theme.spacing.unit
     }
@@ -21,13 +19,16 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 export interface CodeSnippetDataProps {
     lines: string[];
+    className?: string;
 }
 
 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
 export const CodeSnippet = withStyles(styles)(
-    ({ classes, lines }: CodeSnippetProps) =>
-        <Typography component="div" className={classes.root}>
+    ({ classes, lines, className }: CodeSnippetProps) =>
+        <Typography 
+        component="div" 
+        className={classNames(classes.root, className)}>
             {
                 lines.map((line: string, index: number) => {
                     return <Typography key={index} component="pre">{line}</Typography>;
index b8c0a7be93ba96acebca6e77f4c973859b79876d..e8b89f321ca809302bcbdab1f09f4c13d09f6d38 100644 (file)
@@ -23,9 +23,7 @@ const theme = createMuiTheme({
     }
 });
 
-type DefaultCodeSnippet = CodeSnippetDataProps;
-
-export const DefaultCodeSnippet = (props: DefaultCodeSnippet) => 
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) => 
     <MuiThemeProvider theme={theme}>
-        <CodeSnippet lines={props.lines} />
+        <CodeSnippet {...props} />
     </MuiThemeProvider>;
\ No newline at end of file
index e982b454ecf5192e22615941e0dcf161df341f05..a76a86ac7d70213a6dad83a030f3f0e523164ac0 100644 (file)
@@ -36,6 +36,7 @@ import { initWebSocket } from '~/websocket/websocket';
 import { Config } from '~/common/config';
 import { addRouteChangeHandlers } from './routes/route-change-handlers';
 import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { processResourceActionSet } from './views-components/context-menu/action-sets/process-resource-action-set';
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
@@ -54,6 +55,7 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActio
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 
 fetchConfig()
index 107b9e6fb37bb88869b2b09d35dcceb19017a87c..fb28bd05bee5ff360c17cedd01bf2487d7cf4f22 100644 (file)
@@ -26,6 +26,8 @@ export const getResourceUrl = (uuid: string) => {
             return getProjectUrl(uuid);
         case ResourceKind.COLLECTION:
             return getCollectionUrl(uuid);
+        case ResourceKind.PROCESS:
+            return getProcessUrl(uuid);
         default:
             return undefined;
     }
index 7b36b71cf42d7ce6ba289712eac1f2955b047ec4..09e034f5f8b6022e762c902b5d7d4e0cb99411a4 100644 (file)
@@ -32,6 +32,7 @@ export interface Errors {
 export enum CommonResourceServiceError {
     UNIQUE_VIOLATION = 'UniqueViolation',
     OWNERSHIP_CYCLE = 'OwnershipCycle',
+    MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
     UNKNOWN = 'Unknown',
     NONE = 'None'
 }
@@ -121,6 +122,8 @@ export const getCommonResourceServiceError = (errorResponse: any) => {
                 return CommonResourceServiceError.UNIQUE_VIOLATION;
             case /ownership cycle/.test(error):
                 return CommonResourceServiceError.OWNERSHIP_CYCLE;
+            case /Mounts cannot be modified in state 'Final'/.test(error):
+                return CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE;
             default:
                 return CommonResourceServiceError.UNKNOWN;
         }
index 87ba0424be5df6bba68f31dbade878b57559077c..09d4e04e7659aa13a54e137ec7b5571feae980ab 100644 (file)
@@ -9,24 +9,19 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
-export interface CollectionCopyFormDialogData {
-    name: string;
-    ownerUuid: string;
-    uuid: string;
-}
-
 export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
-        const initialData: CollectionCopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
+        const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
         dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
     };
 
-export const copyCollection = (resource: CollectionCopyFormDialogData) =>
+export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
         try {
index bb404b88963b9fbd358db6cdba1aa82a6e7bbf20..d85059d6e1e25a82dfb20c051a55c1c63ffcab0d 100644 (file)
@@ -13,6 +13,7 @@ import { UserResource } from '~/models/user';
 import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { extractUuidKind, ResourceKind } from '~/models/resource';
 import { matchProcessRoute } from '~/routes/routes';
+import { Process } from '~/store/processes/process';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -84,14 +85,10 @@ export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, i
         }
     };
 
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const { location } = getState().router;
-        const pathname = location ? location.pathname : '';
-        const match = matchProcessRoute(pathname); 
-        const uuid = match ? match.params.id : '';
         const resource = {
-            uuid,
+            uuid: process.containerRequest.uuid,
             ownerUuid: '',
             kind: ResourceKind.PROCESS,
             name: '',
@@ -108,6 +105,8 @@ export const resourceKindToContextMenuKind = (uuid: string) => {
             return ContextMenuKind.PROJECT;
         case ResourceKind.COLLECTION:
             return ContextMenuKind.COLLECTION_RESOURCE;
+        case ResourceKind.PROCESS:
+            return ContextMenuKind.PROCESS_RESOURCE;
         case ResourceKind.USER:
             return ContextMenuKind.ROOT_PROJECT;
         default:
diff --git a/src/store/copy-dialog/copy-dialog.ts b/src/store/copy-dialog/copy-dialog.ts
new file mode 100644 (file)
index 0000000..4450cfc
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface CopyFormDialogData {
+    name: string;
+    uuid: string;
+    ownerUuid: string;
+}
\ No newline at end of file
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
new file mode 100644 (file)
index 0000000..de55a2c
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { RootState } from '../store';
+import { Dispatch } from 'redux';
+import { getProcess } from '~/store/processes/process';
+
+export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
+
+export interface ProcessCommandDialogData {
+    command: string;
+    processName: string;
+}
+
+export const openProcessCommandDialog = (processUuid: string) =>
+    (dispatch: Dispatch<any>, getState: () => RootState) => {
+        const process = getProcess(processUuid)(getState().resources);
+        if (process) {
+            const data: ProcessCommandDialogData = {
+                command: process.containerRequest.command.join(' '),
+                processName: process.containerRequest.name,
+            };
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
+        }
+    };
diff --git a/src/store/processes/process-copy-actions.ts b/src/store/processes/process-copy-actions.ts
new file mode 100644 (file)
index 0000000..bb8d8f5
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { initialize, startSubmit } from 'redux-form';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+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 { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
+
+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());
+                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 }));
+            }
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const copyProcess = (resource: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        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 });
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            return process;
+        } catch (e) {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            throw new Error('Could not copy the process.');
+        }
+    };
\ No newline at end of file
diff --git a/src/store/processes/process-move-actions.ts b/src/store/processes/process-move-actions.ts
new file mode 100644 (file)
index 0000000..6df8269
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { startSubmit, stopSubmit, initialize } from 'redux-form';
+import { ServiceRepository } from '~/services/services';
+import { RootState } from '~/store/store';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+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';
+
+export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
+
+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(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 }));
+            }
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const moveProcess = (resource: MoveToFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        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 });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
+            return process;
+        } catch (e) {
+            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 }));
+            }
+            return;
+        }
+    };
\ No newline at end of file
diff --git a/src/store/processes/process-update-actions.ts b/src/store/processes/process-update-actions.ts
new file mode 100644 (file)
index 0000000..92cf032
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { initialize, startSubmit, stopSubmit } from 'redux-form';
+import { RootState } from "~/store/store";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { ServiceRepository } from "~/services/services";
+import { getProcess } from '~/store/processes/process';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export interface ProcessUpdateFormDialogData {
+    uuid: string;
+    name: string;
+}
+
+export const PROCESS_UPDATE_FORM_NAME = 'processUpdateFormName';
+
+export const openProcessUpdateDialog = (resource: ProcessUpdateFormDialogData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const process = getProcess(resource.uuid)(getState().resources);
+        if(process) {
+            dispatch(initialize(PROCESS_UPDATE_FORM_NAME, { ...resource, name: process.containerRequest.name }));
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_UPDATE_FORM_NAME, data: {} }));
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+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 });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+            return updatedProcess;
+        } catch (e) {
+            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 }));
+            }
+            return;
+        }
+    };
\ No newline at end of file
index 7b8fdb25ad83f97ccf12260472e94f0758ed4f64..bb41fa28fc99c89eaf70933d98aa8ede644e1749 100644 (file)
@@ -29,6 +29,9 @@ import * as collectionCopyActions from '~/store/collections/collection-copy-acti
 import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
 import * as collectionMoveActions from '~/store/collections/collection-move-actions';
 import * as processesActions from '../processes/processes-actions';
+import * as processMoveActions from '~/store/processes/process-move-actions';
+import * as processUpdateActions from '~/store/processes/process-update-actions';
+import * as processCopyActions from '~/store/processes/process-copy-actions';
 import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
@@ -36,6 +39,7 @@ import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
 import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
 import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions';
 
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const loadWorkbench = () =>
     async (dispatch: Dispatch, getState: () => RootState) => {
@@ -163,7 +167,7 @@ export const updateCollection = (data: collectionUpdateActions.CollectionUpdateF
         }
     };
 
-export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+export const copyCollection = (data: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
             const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
@@ -197,6 +201,47 @@ export const loadProcess = (uuid: string) =>
 
     };
 
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
+    async (dispatch: Dispatch) => {
+        try {
+            const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+            if (process) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Process has been successfully updated.",
+                    hideDuration: 2000
+                }));
+                dispatch<any>(updateResources([process]));
+                dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            }
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
+export const moveProcess = (data: MoveToFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await dispatch<any>(processMoveActions.moveProcess(data));
+            dispatch<any>(updateResources([process]));
+            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
+export const copyProcess = (data: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await dispatch<any>(processCopyActions.copyProcess(data));
+            dispatch<any>(updateResources([process]));
+            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
 export const loadProcessLog = (uuid: string) =>
     async (dispatch: Dispatch) => {
         const process = await dispatch<any>(processesActions.loadProcess(uuid));
index 95edadfdd1809b23e83aff5ec47043f761b9e30f..755cd7f7b4c7be4b7a1650653c3b7783406c1179 100644 (file)
@@ -19,3 +19,5 @@ export const COPY_NAME_VALIDATION = [require, maxLength(255)];
 export const COPY_FILE_VALIDATION = [require];
 
 export const MOVE_TO_VALIDATION = [require];
+
+export const PROCESS_NAME_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
index 2897455bc4f2b2479e55f52c91729ab13cd5b995..107f1828c609f1d25f65073c4fbdba581282740e 100644 (file)
@@ -11,14 +11,16 @@ import {
 } from "~/components/icon/icon";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+import { openProcessCommandDialog } from '../../../store/processes/process-command-actions';
 
 export const processActionSet: ContextMenuActionSet = [[
     {
         icon: RenameIcon,
         name: "Edit process",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
     },
     {
         icon: ShareIcon,
@@ -30,9 +32,7 @@ export const processActionSet: ContextMenuActionSet = [[
     {
         icon: MoveToIcon,
         name: "Move to",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
     },
     {
         component: ToggleFavoriteAction,
@@ -45,9 +45,7 @@ export const processActionSet: ContextMenuActionSet = [[
     {
         icon: CopyIcon,
         name: "Copy to project",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
     },
     {
         icon: ReRunProcessIcon,
@@ -74,7 +72,7 @@ export const processActionSet: ContextMenuActionSet = [[
         icon: CommandIcon,
         name: "Command",
         execute: (dispatch, resource) => {
-            // add code
+            dispatch<any>(openProcessCommandDialog(resource.uuid));
         }
     },
     {
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
new file mode 100644 (file)
index 0000000..b198523
--- /dev/null
@@ -0,0 +1,59 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+
+export const processResourceActionSet: ContextMenuActionSet = [[
+    {
+        icon: RenameIcon,
+        name: "Edit process",
+        execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
+    },
+    {
+        icon: ShareIcon,
+        name: "Share",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
+    },
+    {
+        component: ToggleFavoriteAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleFavorite(resource)).then(() => {
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+            });
+        }
+    },
+    {
+        icon: CopyIcon,
+        name: "Copy to project",
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
+    },
+    {
+        icon: DetailsIcon,
+        name: "View details",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    },
+    {
+        icon: RemoveIcon,
+        name: "Remove",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    }
+]];
index a545b2bdddfb447ebe5bf43ee768a841d52d36e8..d5c82be2d7f0736243f5e97661e4f7c7ec51d7a4 100644 (file)
@@ -65,5 +65,6 @@ export enum ContextMenuKind {
     COLLECTION = 'Collection',
     COLLECTION_RESOURCE = 'CollectionResource',
     PROCESS = "Process",
+    PROCESS_RESOURCE = 'ProcessResource',
     PROCESS_LOGS = "ProcessLogs"
 }
index c0d4797fa50f68ac89f8dc11b1781b636dfbbdd4..f0075558dfe20549a808a5ebf47840139381dece 100644 (file)
@@ -3,8 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
+import { IconButton, Tabs, Tab, Typography, Grid, Tooltip } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Transition } from 'react-transition-group';
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 import { connect } from 'react-redux';
@@ -20,45 +21,40 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
 
-type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
+type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
-const drawerWidth = 320;
+const DRAWER_WIDTH = 320;
+const SLIDE_TIMEOUT = 500;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        width: 0,
-        overflowX: 'hidden',
-        transition: 'width 0.5s ease',
         background: theme.palette.background.paper,
         borderLeft: `1px solid ${theme.palette.divider}`,
         height: '100%',
+        overflow: 'hidden',
+        transition: `width ${SLIDE_TIMEOUT}ms ease`,
+        width: 0,
     },
     opened: {
-        width: drawerWidth,
+        width: DRAWER_WIDTH,
     },
     container: {
-        width: drawerWidth,
-    },
-    drawerPaper: {
-        position: 'relative',
-        width: drawerWidth
+        maxWidth: 'none',
+        width: DRAWER_WIDTH,
     },
     headerContainer: {
         color: theme.palette.grey["600"],
         margin: `${theme.spacing.unit}px 0`,
-        textAlign: 'center'
+        textAlign: 'center',
     },
     headerIcon: {
-        fontSize: '2.125rem'
-    },
-    headerTitle: {
-        overflowWrap: 'break-word',
-        wordWrap: 'break-word'
+        fontSize: '2.125rem',
     },
     tabContainer: {
-        padding: theme.spacing.unit * 3
-    }
+        overflow: 'auto',
+        padding: theme.spacing.unit * 3,
+    },
 });
 
 const getItem = (resource: DetailsResource): DetailsData => {
@@ -108,50 +104,68 @@ export const DetailsPanel = withStyles(styles)(
                 this.setState({ tabsValue: value });
             }
 
-            renderTabContainer = (children: React.ReactElement<any>) =>
-                <Typography className={this.props.classes.tabContainer} component="div">
-                    {children}
-                </Typography>
-
             render() {
-                const { classes, onCloseDrawer, isOpened, item } = this.props;
-                const { tabsValue } = this.state;
+                const { classes, isOpened } = this.props;
                 return (
-                    <div className={classnames([classes.root, { [classes.opened]: isOpened }])}>
-                        <div className={classes.container}>
-                            <div className={classes.headerContainer}>
-                                <Grid container alignItems='center' justify='space-around'>
-                                    <Grid item xs={2}>
-                                        {item.getIcon(classes.headerIcon)}
-                                    </Grid>
-                                    <Grid item xs={8}>
-                                        <Typography variant="title" className={classes.headerTitle}>
-                                            {item.getTitle()}
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item>
-                                        <IconButton color="inherit" onClick={onCloseDrawer}>
-                                            {<CloseIcon />}
-                                        </IconButton>
-                                    </Grid>
-                                </Grid>
-                            </div>
-                            <Tabs value={tabsValue} onChange={this.handleChange}>
-                                <Tab disableRipple label="Details" />
-                                <Tab disableRipple label="Activity" disabled />
-                            </Tabs>
-                            {tabsValue === 0 && this.renderTabContainer(
-                                <Grid container direction="column">
-                                    {item.getDetails()}
-                                </Grid>
-                            )}
-                            {tabsValue === 1 && this.renderTabContainer(
-                                <Grid container direction="column" />
-                            )}
-                        </div>
-                    </div>
+                    <Grid
+                        container
+                        direction="column"
+                        className={classnames([classes.root, { [classes.opened]: isOpened }])}>
+                        <Transition
+                            in={isOpened}
+                            timeout={SLIDE_TIMEOUT}
+                            unmountOnExit>
+                            {this.renderContent()}
+                        </Transition>
+                    </Grid>
                 );
             }
+
+            renderContent() {
+                const { classes, onCloseDrawer, item } = this.props;
+                const { tabsValue } = this.state;
+                return <Grid
+                    container
+                    direction="column"
+                    item
+                    xs
+                    className={classes.container} >
+                    <Grid
+                        item
+                        className={classes.headerContainer}
+                        container
+                        alignItems='center'
+                        justify='space-around'
+                        wrap="nowrap">
+                        <Grid item xs={2}>
+                            {item.getIcon(classes.headerIcon)}
+                        </Grid>
+                        <Grid item xs={8}>
+                            <Tooltip title={item.getTitle()}>
+                                <Typography variant="title" noWrap>
+                                    {item.getTitle()}
+                                </Typography>
+                            </Tooltip>
+                        </Grid>
+                        <Grid item>
+                            <IconButton color="inherit" onClick={onCloseDrawer}>
+                                <CloseIcon />
+                            </IconButton>
+                        </Grid>
+                    </Grid>
+                    <Grid item>
+                        <Tabs value={tabsValue} onChange={this.handleChange}>
+                            <Tab disableRipple label="Details" />
+                            <Tab disableRipple label="Activity" disabled />
+                        </Tabs>
+                    </Grid>
+                    <Grid item xs className={this.props.classes.tabContainer} >
+                        {tabsValue === 0
+                            ? item.getDetails()
+                            : null}
+                    </Grid>
+                </Grid >;
+            }
         }
     )
 );
index 7fc301fa6c1d44a92e4b38385ddde7683a2920a1..7c335a358c9048cff8af1b136143252a009aad3b 100644 (file)
@@ -19,10 +19,8 @@ export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyPr
         {...props}
     />;
 
-export const CollectionPartialCopyFields = () => <div style={{ display: 'flex' }}>
-    <div>
-        <CollectionNameField />
-        <CollectionDescriptionField />
-    </div>
+export const CollectionPartialCopyFields = () => <div>
+    <CollectionNameField />
+    <CollectionDescriptionField />
     <CollectionProjectPickerField />
 </div>;
similarity index 78%
rename from src/views-components/dialog-copy/dialog-collection-copy.tsx
rename to src/views-components/dialog-copy/dialog-copy.tsx
index 029db578520e197ed37e1f86511e8d20ae69dbbe..415541595c564ff1d3b672062852af92aaf25461 100644 (file)
@@ -9,19 +9,19 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
 import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
 import { TextField } from "~/components/text-field/text-field";
-import { CollectionCopyFormDialogData } from "~/store/collections/collection-copy-actions";
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
-type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CollectionCopyFormDialogData>;
+type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
 
-export const DialogCollectionCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps) =>
     <FormDialog
         dialogTitle='Make a copy'
-        formFields={CollectionCopyFields}
+        formFields={CopyDialogFields}
         submitLabel='Copy'
         {...props}
     />;
 
-const CollectionCopyFields = () => <span>
+const CopyDialogFields = () => <span>
     <Field
         name='name'
         component={TextField}
index 245465fa55edfc2bcb32fab615122ec42c62a90f..41309fdff6952762ed9ae9d9c2922f53dc3e5082 100644 (file)
@@ -5,16 +5,17 @@
 import { compose } from "redux";
 import { withDialog } from "~/store/dialog/with-dialog";
 import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
-import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { COLLECTION_COPY_FORM_NAME } from '~/store/collections/collection-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
 import { copyCollection } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const CopyCollectionDialog = compose(
     withDialog(COLLECTION_COPY_FORM_NAME),
-    reduxForm<CollectionCopyFormDialogData>({
+    reduxForm<CopyFormDialogData>({
         form: COLLECTION_COPY_FORM_NAME,
         onSubmit: (data, dispatch) => {
             dispatch(copyCollection(data));
         }
     })
-)(DialogCollectionCopy);
\ No newline at end of file
+)(DialogCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/copy-process-dialog.ts b/src/views-components/dialog-forms/copy-process-dialog.ts
new file mode 100644 (file)
index 0000000..4ec17c6
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_COPY_FORM_NAME } from '~/store/processes/process-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
+import { copyProcess } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+
+export const CopyProcessDialog = compose(
+    withDialog(PROCESS_COPY_FORM_NAME),
+    reduxForm<CopyFormDialogData>({
+        form: PROCESS_COPY_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(copyProcess(data));
+        }
+    })
+)(DialogCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/move-process-dialog.ts b/src/views-components/dialog-forms/move-process-dialog.ts
new file mode 100644 (file)
index 0000000..baea34b
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from 'redux';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_MOVE_FORM_NAME } from '~/store/processes/process-move-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { moveProcess } from '~/store/workbench/workbench-actions';
+
+export const MoveProcessDialog = compose(
+    withDialog(PROCESS_MOVE_FORM_NAME),
+    reduxForm<MoveToFormDialogData>({
+        form: PROCESS_MOVE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(moveProcess(data));
+        }
+    })
+)(DialogMoveTo);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/update-process-dialog.ts b/src/views-components/dialog-forms/update-process-dialog.ts
new file mode 100644 (file)
index 0000000..12d896d
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { DialogProcessUpdate } from '~/views-components/dialog-update/dialog-process-update';
+import { PROCESS_UPDATE_FORM_NAME, ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { updateProcess } from "~/store/workbench/workbench-actions";
+
+export const UpdateProcessDialog = compose(
+    withDialog(PROCESS_UPDATE_FORM_NAME),
+    reduxForm<ProcessUpdateFormDialogData>({
+        form: PROCESS_UPDATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(updateProcess(data));
+        }
+    })
+)(DialogProcessUpdate);
\ No newline at end of file
diff --git a/src/views-components/dialog-update/dialog-process-update.tsx b/src/views-components/dialog-update/dialog-process-update.tsx
new file mode 100644 (file)
index 0000000..d5bbce6
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProcessNameField } from '~/views-components/form-fields/process-form-fields';
+
+type DialogProcessProps = WithDialogProps<{}> & InjectedFormProps<ProcessUpdateFormDialogData>;
+
+export const DialogProcessUpdate = (props: DialogProcessProps) =>
+    <FormDialog
+        dialogTitle='Edit Process'
+        formFields={ProcessEditFields}
+        submitLabel='Save'
+        {...props}
+    />;
+
+const ProcessEditFields = () => <span>
+    <ProcessNameField />
+</span>;
index af240fc5666c1735bf4938ade1b0bffe3ad358fe..ddd5bceff37d8aabdc60a631f6367d7216bc0abf 100644 (file)
@@ -29,6 +29,6 @@ export const CollectionProjectPickerField = () =>
         validate={COLLECTION_PROJECT_VALIDATION} />;
 
 const ProjectPicker = (props: WrappedFieldProps) =>
-    <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+    <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
         <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
     </div>;
diff --git a/src/views-components/form-fields/process-form-fields.tsx b/src/views-components/form-fields/process-form-fields.tsx
new file mode 100644 (file)
index 0000000..cf471b6
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field, WrappedFieldProps } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
+
+export const ProcessNameField = () =>
+    <Field
+        name='name'
+        component={TextField}
+        validate={PROCESS_NAME_VALIDATION}
+        label="Process Name" />;
diff --git a/src/views-components/process-command-dialog/process-command-dialog.tsx b/src/views-components/process-command-dialog/process-command-dialog.tsx
new file mode 100644 (file)
index 0000000..4bde68d
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { PROCESS_COMMAND_DIALOG_NAME } from '~/store/processes/process-command-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessCommandDialogData } from '~/store/processes/process-command-actions';
+import { DefaultCodeSnippet } from "~/components/default-code-snippet/default-code-snippet";
+import { compose } from 'redux';
+
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    codeSnippet: {
+        marginLeft: theme.spacing.unit * 3,
+        marginRight: theme.spacing.unit * 3,
+    }
+});
+
+export const ProcessCommandDialog = compose(
+    withDialog(PROCESS_COMMAND_DIALOG_NAME),
+    withStyles(styles),
+)(
+    (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
+        <Dialog
+            open={props.open}
+            maxWidth="md"
+            onClose={props.closeDialog}
+            style={{ alignSelf: 'stretch' }}>
+            <DialogTitle>{`Command - ${props.data.processName}`}</DialogTitle>
+            <DefaultCodeSnippet
+                className={props.classes.codeSnippet}
+                lines={[props.data.command]} />
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    onClick={props.closeDialog}>
+                    Close
+                </Button>
+            </DialogActions>
+        </Dialog>
+);
\ No newline at end of file
index ff6320eeaf03f76c711044b3bfc819a0c5c6e837..d02fc02ce1431c74a2aefdaf4137991d0552ba61 100644 (file)
@@ -3,10 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
-import { CodeSnippet, CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { MuiThemeProvider, createMuiTheme, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
+import { CodeSnippet } from '~/components/code-snippet/code-snippet';
 import grey from '@material-ui/core/colors/grey';
 
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = () => ({
+    codeSnippet: {
+        maxHeight: '550px',
+    }
+});
+
 const theme = createMuiTheme({
     overrides: {
         MuiTypography: {
@@ -23,9 +31,12 @@ const theme = createMuiTheme({
     }
 });
 
-type ProcessLogCodeSnippet = CodeSnippetDataProps;
+interface ProcessLogCodeSnippetProps {
+    lines: string[];
+}
 
-export const ProcessLogCodeSnippet = (props: ProcessLogCodeSnippet) => 
-    <MuiThemeProvider theme={theme}>
-        <CodeSnippet lines={props.lines} />
-    </MuiThemeProvider>;
\ No newline at end of file
+export const ProcessLogCodeSnippet = withStyles(styles)(
+    (props: ProcessLogCodeSnippetProps & WithStyles<CssRules>) =>
+        <MuiThemeProvider theme={theme}>
+            <CodeSnippet lines={props.lines} className={props.classes.codeSnippet} />
+        </MuiThemeProvider>);
\ No newline at end of file
index 66811f47b89209191c7de02d329efa4d2259da49..626568d179e2e6a4428f52db8952bcd5d95f1483 100644 (file)
@@ -53,10 +53,18 @@ interface ProcessLogMainCardDataProps {
     process: Process;
 }
 
-export type ProcessLogMainCardProps = ProcessLogMainCardDataProps & CodeSnippetDataProps & ProcessLogFormDataProps & ProcessLogFormActionProps;
+export interface ProcessLogMainCardActionProps {
+    onContextMenu: (event: React.MouseEvent<any>, process: Process) => void;
+}
+
+export type ProcessLogMainCardProps = ProcessLogMainCardDataProps
+    & ProcessLogMainCardActionProps
+    & CodeSnippetDataProps
+    & ProcessLogFormDataProps
+    & ProcessLogFormActionProps;
 
 export const ProcessLogMainCard = withStyles(styles)(
-    ({ classes, process, selectedFilter, filters, onChange, lines }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
+    ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
         <Grid item xs={12}>
             <Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
                 <BackIcon className={classes.backIcon} /> Back
@@ -65,34 +73,35 @@ export const ProcessLogMainCard = withStyles(styles)(
                 <CardHeader
                     avatar={<ProcessIcon className={classes.iconHeader} />}
                     action={
-                        <div>
-                            <IconButton aria-label="More options">
-                                <Tooltip title="More options">
-                                    <MoreOptionsIcon />
-                                </Tooltip>
-                            </IconButton>
-                        </div>
-                    }
+                        <IconButton onClick={event => onContextMenu(event, process)} aria-label="More options">
+                            <Tooltip title="More options">
+                                <MoreOptionsIcon />
+                            </Tooltip>
+                        </IconButton>}
                     title={
                         <Tooltip title={process.containerRequest.name} placement="bottom-start">
                             <Typography noWrap variant="title" className={classes.title}>
                                 {process.containerRequest.name}
                             </Typography>
-                        </Tooltip>
-                    }
+                        </Tooltip>}
                     subheader={process.containerRequest.description} />
                 <CardContent>
                     {lines.length > 0
-                        ? < Grid container spacing={24} alignItems='center'>
-                            <Grid item xs={6}>
-                                <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
-                            </Grid>
-                            <Grid item xs={6} className={classes.link}>
-                                <Typography component='div'>
-                                    Go to Log collection
+                        ? < Grid
+                            container
+                            spacing={24}
+                            direction='column'>
+                            <Grid container item>
+                                <Grid item xs={6}>
+                                    <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
+                                </Grid>
+                                <Grid item xs={6} className={classes.link}>
+                                    <Typography component='div'>
+                                        Go to Log collection
                                 </Typography>
+                                </Grid>
                             </Grid>
-                            <Grid item xs={12}>
+                            <Grid item xs>
                                 <ProcessLogCodeSnippet lines={lines} />
                             </Grid>
                         </Grid>
index 0845a4109780d1f8995e5d47d80eca99eead0b43..38870c402759814d3e9b4a0d5abaed08b5d4991c 100644 (file)
@@ -10,14 +10,13 @@ import { ProcessLogFormDataProps, ProcessLogFormActionProps } from '~/views/proc
 import { DefaultView } from '~/components/default-view/default-view';
 import { ProcessIcon } from '~/components/icon/icon';
 import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { ProcessLogMainCardActionProps } from './process-log-main-card';
 
 export type ProcessLogPanelRootDataProps = {
     process?: Process;
 } & ProcessLogFormDataProps & CodeSnippetDataProps;
 
-export type ProcessLogPanelRootActionProps = {
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
-} & ProcessLogFormActionProps;
+export type ProcessLogPanelRootActionProps = ProcessLogMainCardActionProps & ProcessLogFormActionProps;
 
 export type ProcessLogPanelRootProps = ProcessLogPanelRootDataProps & ProcessLogPanelRootActionProps;
 
index 2b2d6842774fdc168a200864df22dc7df15bfe48..d578e784566b5d1c1d5bdd3caeb9672998c5e95f 100644 (file)
@@ -2,13 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
 import { RootState } from '~/store/store';
 import { connect } from 'react-redux';
 import { getProcess } from '~/store/processes/process';
 import { Dispatch } from 'redux';
 import { openProcessContextMenu } from '~/store/context-menu/context-menu-actions';
-import { matchProcessLogRoute } from '~/routes/routes';
 import { ProcessLogPanelRootDataProps, ProcessLogPanelRootActionProps, ProcessLogPanelRoot } from './process-log-panel-root';
 import { getProcessPanelLogs } from '~/store/process-logs-panel/process-logs-panel';
 import { setProcessLogsPanelFilter } from '~/store/process-logs-panel/process-logs-panel-actions';
@@ -39,10 +37,10 @@ const mapStateToProps = (state: RootState): ProcessLogPanelRootDataProps => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
-        dispatch<any>(openProcessContextMenu(event));
+    onContextMenu: (event, process) => {
+        dispatch<any>(openProcessContextMenu(event, process));
     },
-    onChange: (filter: FilterOption) => {
+    onChange: filter => {
         dispatch(setProcessLogsPanelFilter(filter.value));
     }
 });
index d12704bcf4b69c87ec59283dc824feba3e1dd308..ab8af36ffbb5e619e69fa22e2fbbb62b07d0a634 100644 (file)
@@ -20,19 +20,19 @@ export interface ProcessPanelRootDataProps {
 }
 
 export interface ProcessPanelRootActionProps {
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
     onToggle: (status: string) => void;
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
 
-export const ProcessPanelRoot = (props: ProcessPanelRootProps) =>
-    props.process
+export const ProcessPanelRoot = ({process, ...props}: ProcessPanelRootProps) =>
+    process
         ? <Grid container spacing={16} alignItems="stretch">
             <Grid item sm={12} md={7}>
                 <ProcessInformationCard
-                    process={props.process}
-                    onContextMenu={props.onContextMenu} />
+                    process={process}
+                    onContextMenu={event => props.onContextMenu(event, process)} />
             </Grid>
             <Grid item sm={12} md={5}>
                 <SubprocessesCard
index b3e36ab77c1742c646db51d2672b76d198a2c0b7..4f283a6c9cadb56826b74b47d09a19501a0253df 100644 (file)
@@ -27,8 +27,8 @@ const mapStateToProps = ({ router, resources, processPanel }: RootState): Proces
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-    onContextMenu: event => {
-        dispatch<any>(openProcessContextMenu(event));
+    onContextMenu: (event, process) => {
+        dispatch<any>(openProcessContextMenu(event, process));
     },
     onToggle: status => {
         dispatch<any>(toggleProcessPanelFilter(status));
index 6e00deb0f1a0963ce8f69280ec84ed86c0c072dd..d3f87701fc73724799a8968df0b5da29d394d4bb 100644 (file)
@@ -9,14 +9,16 @@ import { Process } from '~/store/processes/process';
 
 export interface ProcessSubprocessesDataProps {
     subprocesses: Array<Process>;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
 }
 
 export const ProcessSubprocesses = ({ onContextMenu, subprocesses }: ProcessSubprocessesDataProps) => {
     return <Grid container spacing={16}>
         {subprocesses.map(subprocess =>
             <Grid item xs={12} sm={6} md={4} lg={2} key={subprocess.containerRequest.uuid}>
-                <ProcessSubprocessesCard onContextMenu={onContextMenu} subprocess={subprocess} />
+                <ProcessSubprocessesCard
+                    onContextMenu={event => onContextMenu(event, subprocess)}
+                    subprocess={subprocess} />
             </Grid>
         )}
     </Grid>;
index e075680de4981262cc4147eb61f755d9069646d2..fee7652b0ae89fbd619037ecfa6fc81855cd75a3 100644 (file)
@@ -30,17 +30,20 @@ import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
 import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
 import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
 import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { CopyProcessDialog } from '~/views-components/dialog-forms/copy-process-dialog';
 import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProcessDialog } from '~/views-components/dialog-forms/update-process-dialog';
 import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
+import { MoveProcessDialog } from '~/views-components/dialog-forms/move-process-dialog';
 import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
 import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
 import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
-
 import { TrashPanel } from "~/views/trash-panel/trash-panel";
-import { MainContentBar } from '../../views-components/main-content-bar/main-content-bar';
+import { MainContentBar } from '~/views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
 import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
+import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
 
 type CssRules = 'root' | 'contentWrapper' | 'content' | 'appBar';
 
@@ -144,21 +147,25 @@ export const Workbench = withStyles(styles)(
                             </Grid>}
                     </Grid>
                     <ContextMenu />
-                    <Snackbar />
-                    <CreateProjectDialog />
+                    <CopyCollectionDialog />
+                    <CopyProcessDialog />
                     <CreateCollectionDialog />
-                    <RenameFileDialog />
-                    <PartialCopyCollectionDialog />
+                    <CreateProjectDialog />
+                    <CurrentTokenDialog />
                     <FileRemoveDialog />
-                    <CopyCollectionDialog />
                     <FileRemoveDialog />
-                    <MultipleFilesRemoveDialog />
-                    <UpdateCollectionDialog />
                     <FilesUploadCollectionDialog />
-                    <UpdateProjectDialog />
                     <MoveCollectionDialog />
+                    <MoveProcessDialog />
                     <MoveProjectDialog />
-                    <CurrentTokenDialog />
+                    <MultipleFilesRemoveDialog />
+                    <PartialCopyCollectionDialog />
+                    <ProcessCommandDialog />
+                    <RenameFileDialog />
+                    <Snackbar />
+                    <UpdateCollectionDialog />
+                    <UpdateProcessDialog />
+                    <UpdateProjectDialog />
                 </>;
             }
 
index 67c12647b4625337eae116a1f6feea7f9322da8e..359927100d4493eca58c01ca52ccc458a55361eb 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -6307,7 +6307,7 @@ react-test-renderer@^16.0.0-0:
     prop-types "^15.6.0"
     react-is "^16.4.1"
 
-react-transition-group@^2.2.1:
+react-transition-group@2.4.0, react-transition-group@^2.2.1:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
   dependencies: