16070: Replace process command dialog with command card
authorStephen Smith <stephen@curii.com>
Thu, 21 Jul 2022 01:06:03 +0000 (21:06 -0400)
committerStephen Smith <stephen@curii.com>
Thu, 21 Jul 2022 01:06:03 +0000 (21:06 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

src/components/code-snippet/code-snippet.tsx
src/store/processes/process-command-actions.ts [deleted file]
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/process-command-dialog/process-command-dialog.tsx [deleted file]
src/views/process-panel/process-cmd-card.tsx [new file with mode: 0644]
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/workbench/workbench.tsx

index f0a2b2131fcfbd4bfd0592c24fbebd68b1d44f0b..f156e3e8e6e7ad26258662617f6ce3aafd60b960 100644 (file)
@@ -33,10 +33,8 @@ export const CodeSnippet = withStyles(styles)(
         <Typography
         component="div"
         className={classNames(classes.root, className)}>
         <Typography
         component="div"
         className={classNames(classes.root, className)}>
-            {
-                lines.map((line: string, index: number) => {
-                    return <Typography key={index} className={apiResponse ? classes.space : className} component="pre">{line}</Typography>;
-                })
-            }
+            <Typography className={apiResponse ? classes.space : className} component="pre">
+                {lines.join('\n')}
+            </Typography>
         </Typography>
         </Typography>
-    );
\ No newline at end of file
+    );
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
deleted file mode 100644 (file)
index d81a7c4..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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/store';
-import { Dispatch } from 'redux';
-import { getProcess } from 'store/processes/process';
-import shellescape from 'shell-escape';
-
-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: shellescape(process.containerRequest.command),
-                processName: process.containerRequest.name,
-            };
-            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
-        }
-    };
index 55b2d31fab2202ded4367af0dd61bc80182cc351..65e29bef7dcb21a38bfd3a71240958a8902777ca 100644 (file)
@@ -20,7 +20,6 @@ import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { openProcessInputDialog } from "store/processes/process-input-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { openProcessInputDialog } from "store/processes/process-input-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
-import { openProcessCommandDialog } from "store/processes/process-command-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
 import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
 import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
@@ -69,13 +68,6 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             }
         }
     },
             }
         }
     },
-    {
-        icon: CommandIcon,
-        name: "Command",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessCommandDialog(resource.uuid));
-        }
-    },
     {
         icon: DetailsIcon,
         name: "View details",
     {
         icon: DetailsIcon,
         name: "View details",
@@ -135,4 +127,4 @@ export const processResourceAdminActionSet: ContextMenuActionSet = [[
             });
         }
     },
             });
         }
     },
-]];
\ No newline at end of file
+]];
diff --git a/src/views-components/process-command-dialog/process-command-dialog.tsx b/src/views-components/process-command-dialog/process-command-dialog.tsx
deleted file mode 100644 (file)
index 7695837..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles, Tooltip, IconButton, CardHeader } 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';
-import CopyToClipboard from "react-copy-to-clipboard";
-import { CopyIcon } from 'components/icon/icon';
-
-type CssRules = 'codeSnippet' | 'copyToClipboard';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    codeSnippet: {
-        marginLeft: theme.spacing.unit * 3,
-        marginRight: theme.spacing.unit * 3,
-    },
-    copyToClipboard: {
-        marginRight: theme.spacing.unit,
-    }
-});
-
-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' }}>
-            <CardHeader
-                title={`Command - ${props.data.processName}`}
-                action={
-                    <Tooltip title="Copy to clipboard">
-                        <CopyToClipboard text={props.data.command}>
-                            <IconButton className={props.classes.copyToClipboard}>
-                                <CopyIcon />
-                            </IconButton>
-                        </CopyToClipboard>
-                    </Tooltip>
-                } />
-            <DefaultCodeSnippet
-                className={props.classes.codeSnippet}
-                lines={[props.data.command]} />
-            <DialogActions>
-                <Button
-                    variant='text'
-                    color='primary'
-                    onClick={props.closeDialog}>
-                    Close
-                </Button>
-            </DialogActions>
-        </Dialog>
-);
\ No newline at end of file
diff --git a/src/views/process-panel/process-cmd-card.tsx b/src/views/process-panel/process-cmd-card.tsx
new file mode 100644 (file)
index 0000000..36a0128
--- /dev/null
@@ -0,0 +1,133 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardHeader,
+    IconButton,
+    CardContent,
+    Tooltip,
+    Typography,
+    Grid,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, CommandIcon, CopyIcon } from 'components/icon/icon';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code-snippet';
+import { Process } from 'store/processes/process';
+import shellescape from 'shell-escape';
+import CopyToClipboard from 'react-copy-to-clipboard';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    card: {
+        height: '100%'
+    },
+    header: {
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
+    },
+    iconHeader: {
+        fontSize: '1.875rem',
+        color: theme.customs.colors.green700,
+    },
+    avatar: {
+        alignSelf: 'flex-start',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+    content: {
+        padding: theme.spacing.unit * 1.0,
+        paddingTop: theme.spacing.unit * 0.5,
+        '&:last-child': {
+            paddingBottom: theme.spacing.unit * 1,
+        }
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+});
+
+export interface ProcessCmdCardDataProps {
+  process: Process;
+  onCopy: (text: string) => void;
+}
+
+type ProcessCmdCardProps = ProcessCmdCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessCmdCard = withStyles(styles)(
+  ({
+    process,
+    onCopy,
+    classes,
+    doHidePanel,
+  }: ProcessCmdCardProps) => {
+    const command = process.containerRequest.command.map((v) =>
+      shellescape([v]) // Escape each arg separately
+    );
+
+    let formattedCommand = [...command];
+    formattedCommand.forEach((item, i, arr) => {
+      // Indent lines after the first
+      const indent = i > 0 ? '  ' : '';
+      // Escape newlines on every non-last arg when there are multiple lines
+      const lineBreak = arr.length > 1 && i < arr.length - 1 ? ' \\' : '';
+      arr[i] = `${indent}${item}${lineBreak}`;
+    });
+
+    return (
+      <Card className={classes.card}>
+        <CardHeader
+          className={classes.header}
+          classes={{
+            content: classes.title,
+            avatar: classes.avatar,
+          }}
+          avatar={<CommandIcon className={classes.iconHeader} />}
+          title={
+            <Typography noWrap variant="h6" color="inherit">
+              Command
+            </Typography>
+          }
+          action={
+            <Grid container direction="row" alignItems="center">
+              <Grid item>
+                <Tooltip title="Copy to clipboard" disableFocusListener>
+                  <IconButton>
+                    <CopyToClipboard
+                      text={command.join(" ")}
+                      onCopy={() => onCopy("Command copied to clipboard")}
+                    >
+                      <CopyIcon />
+                    </CopyToClipboard>
+                  </IconButton>
+                </Tooltip>
+              </Grid>
+              <Grid item>
+                {doHidePanel && (
+                  <Tooltip
+                    title={`Close Command Panel`}
+                    disableFocusListener
+                  >
+                    <IconButton onClick={doHidePanel}>
+                      <CloseIcon />
+                    </IconButton>
+                  </Tooltip>
+                )}
+              </Grid>
+            </Grid>
+          }
+        />
+        <CardContent className={classes.content}>
+          <DefaultCodeSnippet lines={formattedCommand} />
+        </CardContent>
+      </Card>
+    );
+  }
+);
index 4f95d0d88726f1b4354138946fb113ae749d786c..f8ff84304dcb3fb4acc7554ef0f26882ef9cc6d6 100644 (file)
@@ -15,6 +15,7 @@ import { ProcessDetailsCard } from './process-details-card';
 import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
 import { ProcessLogsCard } from './process-log-card';
 import { FilterOption } from 'views/process-panel/process-log-form';
 import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
 import { ProcessLogsCard } from './process-log-card';
 import { FilterOption } from 'views/process-panel/process-log-form';
+import { ProcessCmdCard } from './process-cmd-card';
 
 type CssRules = 'root';
 
 
 type CssRules = 'root';
 
@@ -37,13 +38,14 @@ export interface ProcessPanelRootActionProps {
     cancelProcess: (uuid: string) => void;
     onLogFilterChange: (filter: FilterOption) => void;
     navigateToLog: (uuid: string) => void;
     cancelProcess: (uuid: string) => void;
     onLogFilterChange: (filter: FilterOption) => void;
     navigateToLog: (uuid: string) => void;
-    onLogCopyToClipboard: (uuid: string) => void;
+    onCopyToClipboard: (uuid: string) => void;
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
 
 const panelsData: MPVPanelState[] = [
     {name: "Details"},
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
 
 const panelsData: MPVPanelState[] = [
     {name: "Details"},
+    {name: "Command"},
     {name: "Logs", visible: true},
     {name: "Subprocesses"},
 ];
     {name: "Logs", visible: true},
     {name: "Subprocesses"},
 ];
@@ -59,9 +61,14 @@ export const ProcessPanelRoot = withStyles(styles)(
                     cancelProcess={props.cancelProcess}
                 />
             </MPVPanelContent>
                     cancelProcess={props.cancelProcess}
                 />
             </MPVPanelContent>
+            <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+                <ProcessCmdCard
+                    onCopy={props.onCopyToClipboard}
+                    process={process} />
+            </MPVPanelContent>
             <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
                 <ProcessLogsCard
             <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
                 <ProcessLogsCard
-                    onCopy={props.onLogCopyToClipboard}
+                    onCopy={props.onCopyToClipboard}
                     process={process}
                     lines={getProcessPanelLogs(processLogsPanel)}
                     selectedFilter={{
                     process={process}
                     lines={getProcessPanelLogs(processLogsPanel)}
                     selectedFilter={{
index de6b13b3163c7f2ffaebb2181f8e235b73dbc416..7afaa04d94b95f90e47181f2a449985c0879fd62 100644 (file)
@@ -36,7 +36,7 @@ const mapStateToProps = ({ router, resources, processPanel, processLogsPanel }:
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-    onLogCopyToClipboard: (message: string) => {
+    onCopyToClipboard: (message: string) => {
         dispatch<any>(snackbarActions.OPEN_SNACKBAR({
             message,
             hideDuration: 2000,
         dispatch<any>(snackbarActions.OPEN_SNACKBAR({
             message,
             hideDuration: 2000,
index 28fae4cd6b1b5a4b7a668d798c55b3271ad4c9f5..a6c49e348495e6f48a21e1d8a99e6a78016a1e83 100644 (file)
@@ -33,7 +33,6 @@ import { MoveProjectDialog } from 'views-components/dialog-forms/move-project-di
 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 { 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 { ProcessCommandDialog } from 'views-components/process-command-dialog/process-command-dialog';
 import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
 import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
@@ -241,7 +240,6 @@ export const WorkbenchPanel =
             <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <PartialCopyToCollectionDialog />
             <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <PartialCopyToCollectionDialog />
-            <ProcessCommandDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />