Merge branch 'main' of git.arvados.org:arvados-workbench2 into 19079-search-results...
authorStephen Smith <stephen@curii.com>
Thu, 28 Jul 2022 20:04:24 +0000 (16:04 -0400)
committerStephen Smith <stephen@curii.com>
Thu, 28 Jul 2022 20:04:24 +0000 (16:04 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

12 files changed:
package.json
src/components/code-snippet/code-snippet.tsx
src/components/default-code-snippet/default-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-components/token-dialog/token-dialog.test.tsx
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
yarn.lock

index a8b3ee819a860fbb06aff7effef0f6f9198a89af..9e663ca6ac5a440946e91f1f3a50ce030db355e0 100644 (file)
@@ -22,7 +22,7 @@
     "@types/react-virtualized-auto-sizer": "1.0.0",
     "@types/react-window": "1.8.2",
     "@types/redux-form": "7.4.12",
-    "@types/shell-quote": "1.6.0",
+    "@types/shell-escape": "^0.2.0",
     "axios": "^0.21.1",
     "babel-core": "6.26.3",
     "babel-runtime": "6.26.0",
@@ -71,7 +71,7 @@
     "redux-thunk": "2.3.0",
     "reselect": "4.0.0",
     "set-value": "2.0.1",
-    "shell-quote": "1.6.1",
+    "shell-escape": "^0.2.0",
     "sinon": "7.3",
     "tslint": "5.20.0",
     "tslint-etc": "1.6.0",
index f0a2b2131fcfbd4bfd0592c24fbebd68b1d44f0b..83c378b899ec11a884cb6d213b932423a14ba0aa 100644 (file)
@@ -3,9 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { StyleRulesCallback, WithStyles, Typography, withStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Typography, withStyles, Link } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 import classNames from 'classnames';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { Dispatch } from 'redux';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
 
 type CssRules = 'root' | 'space';
 
@@ -24,19 +29,57 @@ export interface CodeSnippetDataProps {
     lines: string[];
     className?: string;
     apiResponse?: boolean;
+    linked?: boolean;
+}
+
+interface CodeSnippetAuthProps {
+    auth: FederationConfig;
 }
 
 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
-export const CodeSnippet = withStyles(styles)(
-    ({ classes, lines, className, apiResponse }: CodeSnippetProps) =>
+const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
         <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">
+                {linked ?
+                    lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
+                    lines.join('\n')
+                }
+            </Typography>
         </Typography>
-    );
\ No newline at end of file
+    ));
+
+const renderLinks = (auth: FederationConfig, dispatch: Dispatch) => (text: string): JSX.Element => {
+    // Matches UUIDs & PDHs
+    const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
+    const links = text.match(REGEX);
+    if (!links) {
+        return <>{text}</>;
+    }
+    return <>
+        {text.split(REGEX).map((part, index) =>
+            <React.Fragment key={index}>
+                {part}
+                {links[index] &&
+                <Link onClick={() => {
+                    const url = getNavUrl(links[index], auth)
+                    if (url) {
+                        window.open(`${window.location.origin}${url}`, '_blank');
+                    } else {
+                        dispatch(navigationNotAvailable(links[index]));
+                    }
+                }}
+                    style={ {cursor: 'pointer'} }>
+                    {links[index]}
+                </Link>}
+            </React.Fragment>
+        )}
+    </>;
+  };
index 7ba97db49fbef65b723c51327ac47e94fcf6e7fe..bdcfc10f644b8c4f7ff38bed436bc521333d7109 100644 (file)
@@ -6,8 +6,9 @@ import React from 'react';
 import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
 import { CodeSnippet, CodeSnippetDataProps } from 'components/code-snippet/code-snippet';
 import grey from '@material-ui/core/colors/grey';
+import { themeOptions } from 'common/custom-theme';
 
-const theme = createMuiTheme({
+const theme = createMuiTheme(Object.assign({}, themeOptions, {
     overrides: {
         MuiTypography: {
             body1: {
@@ -22,9 +23,9 @@ const theme = createMuiTheme({
         fontFamily: 'monospace',
         useNextVariants: true,
     }
-});
+}));
 
-export const DefaultCodeSnippet = (props: CodeSnippetDataProps) => 
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
     <MuiThemeProvider theme={theme}>
         <CodeSnippet {...props} />
-    </MuiThemeProvider>;
\ No newline at end of file
+    </MuiThemeProvider>;
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
deleted file mode 100644 (file)
index 9dec9b3..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 { quote } from 'shell-quote';
-
-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: quote(process.containerRequest.command),
-                processName: process.containerRequest.name,
-            };
-            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
-        }
-    };
index 55b2d31fab2202ded4367af0dd61bc80182cc351..a7a7590148521e480a5daf7822319b9c93effecf 100644 (file)
@@ -7,7 +7,7 @@ import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
     RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
-    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon, CommandIcon,
+    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon,
     AdvancedIcon
 } from "components/icon/icon";
 import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
@@ -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 { 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";
@@ -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",
@@ -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
index d2ff77e3d41777b1dad76806c6150b29037e8d64..400bb1e68724d88d58b0f42819d930755432dc1f 100644 (file)
@@ -16,6 +16,8 @@ import { mount, configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
 import CopyToClipboard from 'react-copy-to-clipboard';
 import { TokenDialogComponent } from './token-dialog';
+import { combineReducers, createStore } from 'redux';
+import { Provider } from 'react-redux';
 
 configure({ adapter: new Adapter() });
 
@@ -24,6 +26,7 @@ jest.mock('toggle-selection', () => () => () => null);
 describe('<CurrentTokenDialog />', () => {
   let props;
   let wrapper;
+  let store;
 
   beforeEach(() => {
     props = {
@@ -33,11 +36,25 @@ describe('<CurrentTokenDialog />', () => {
       open: true,
       dispatch: jest.fn(),
     };
+
+    const initialAuthState = {
+      localCluster: "zzzzz",
+      remoteHostsConfig: {},
+      sessions: {},
+    };
+
+    store = createStore(combineReducers({
+      auth: (state: any = initialAuthState, action: any) => state,
+    }));
   });
 
   describe('Get API Token dialog', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>
+      );
     });
 
     it('should include API host and token', () => {
@@ -51,7 +68,10 @@ describe('<CurrentTokenDialog />', () => {
 
       const someDate = '2140-01-01T00:00:00.000Z'
       props.tokenExpiration = new Date(someDate);
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain(props.tokenExpiration.toLocaleString());
     });
 
@@ -60,14 +80,20 @@ describe('<CurrentTokenDialog />', () => {
       expect(wrapper.html()).not.toContain('GET NEW TOKEN');
 
       props.canCreateNewTokens = true;
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain('GET NEW TOKEN');
     });
   });
 
   describe('copy to clipboard button', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
     });
 
     it('should copy API TOKEN to the clipboard', () => {
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..4143501
--- /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
+    },
+});
+
+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} linked />
+        </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 { ProcessCmdCard } from './process-cmd-card';
 
 type CssRules = 'root';
 
@@ -37,13 +38,14 @@ export interface ProcessPanelRootActionProps {
     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"},
+    {name: "Command"},
     {name: "Logs", visible: true},
     {name: "Subprocesses"},
 ];
@@ -59,9 +61,14 @@ export const ProcessPanelRoot = withStyles(styles)(
                     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
-                    onCopy={props.onLogCopyToClipboard}
+                    onCopy={props.onCopyToClipboard}
                     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 => ({
-    onLogCopyToClipboard: (message: string) => {
+    onCopyToClipboard: (message: string) => {
         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 { 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';
@@ -241,7 +240,6 @@ export const WorkbenchPanel =
             <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <PartialCopyToCollectionDialog />
-            <ProcessCommandDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
index 13ea553a0166a9b05bed5c5af8897dda21efab20..6dfb5b18b8aae518b209c72c9888a5fbb65d00cc 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2817,10 +2817,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/shell-quote@npm:1.6.0":
-  version: 1.6.0
-  resolution: "@types/shell-quote@npm:1.6.0"
-  checksum: 5d9f4e35c8df32d9994f8ae2f1a1fe8a6b7ee96794f803e0904ceae7ad7255a214954e85cd75bd847fe77458d3746430522e87237438f223b7d72a23c4928c0e
+"@types/shell-escape@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "@types/shell-escape@npm:0.2.0"
+  checksum: 020696ed313eeb02deb2abcc581e8b570be6f9ee662892339965b524bb4fbdc9a97b6520d914117740ec11147b0b1aa52358b8e03fa214c2da99743adb196853
   languageName: node
   linkType: hard
 
@@ -3600,13 +3600,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-filter@npm:~0.0.0":
-  version: 0.0.1
-  resolution: "array-filter@npm:0.0.1"
-  checksum: 0e9afdf5e248c45821c6fe1232071a13a3811e1902c2c2a39d12e4495e8b0b25739fd95bffbbf9884b9693629621f6077b4ae16207b8f23d17710fc2465cebbb
-  languageName: node
-  linkType: hard
-
 "array-find-index@npm:^1.0.1":
   version: 1.0.2
   resolution: "array-find-index@npm:1.0.2"
@@ -3648,20 +3641,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-map@npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-map@npm:0.0.0"
-  checksum: 30d73fdc99956c8bd70daea40db5a7d78c5c2c75a03c64fc77904885e79adf7d5a0595076534f4e58962d89435f0687182ac929e65634e3d19931698cbac8149
-  languageName: node
-  linkType: hard
-
-"array-reduce@npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-reduce@npm:0.0.0"
-  checksum: d6226325271f477e3dd65b4d40db8597735b8d08bebcca4972e52d3c173d6c697533664fa8865789ea2d076bdaf1989bab5bdfbb61598be92074a67f13057c3a
-  languageName: node
-  linkType: hard
-
 "array-union@npm:^1.0.1":
   version: 1.0.2
   resolution: "array-union@npm:1.0.2"
@@ -3769,7 +3748,7 @@ __metadata:
     "@types/redux-devtools": 3.0.44
     "@types/redux-form": 7.4.12
     "@types/redux-mock-store": 1.0.2
-    "@types/shell-quote": 1.6.0
+    "@types/shell-escape": ^0.2.0
     "@types/sinon": 7.5
     "@types/uuid": 3.4.4
     axios: ^0.21.1
@@ -3829,7 +3808,7 @@ __metadata:
     redux-thunk: 2.3.0
     reselect: 4.0.0
     set-value: 2.0.1
-    shell-quote: 1.6.1
+    shell-escape: ^0.2.0
     sinon: 7.3
     ts-mock-imports: 1.3.7
     tslint: 5.20.0
@@ -16502,15 +16481,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"shell-quote@npm:1.6.1":
-  version: 1.6.1
-  resolution: "shell-quote@npm:1.6.1"
-  dependencies:
-    array-filter: ~0.0.0
-    array-map: ~0.0.0
-    array-reduce: ~0.0.0
-    jsonify: ~0.0.0
-  checksum: 982a4fdf2d474f0dc40885de4222f100ba457d7c75d46b532bf23b01774b8617bc62522c6825cb1fa7dd4c54c18e9dcbae7df2ca8983101841b6f2e6a7cacd2f
+"shell-escape@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "shell-escape@npm:0.2.0"
+  checksum: 0d87f1ae22ad22a74e148348ceaf64721e3024f83c90afcfb527318ce10ece654dd62e103dd89a242f2f4e4ce3cecdef63e3d148c40e5fabca8ba6c508f97d9f
   languageName: node
   linkType: hard