Bläddra i källkod

HPCC-26568 Replace native JS confirm with React component

Signed-off-by: Jeremy Clements <jeremy.clements@lexisnexisrisk.com>
Jeremy Clements 3 år sedan
förälder
incheckning
23de64fff1

+ 89 - 85
esp/src/src-react/components/DFUWorkunitDetails.tsx

@@ -5,8 +5,9 @@ import { SizeMe } from "react-sizeme";
 import nlsHPCC from "src/nlsHPCC";
 import * as FileSpray from "src/FileSpray";
 import * as ESPDFUWorkunit from "src/ESPDFUWorkunit";
+import { useConfirm } from "../hooks/confirm";
 import { pivotItemStyle } from "../layouts/pivot";
-import { pushUrl } from "../util/history";
+import { pushUrl, replaceUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
 import { TableGroup } from "./forms/Groups";
 import { XMLSourceEditor } from "./SourceEditor";
@@ -29,6 +30,14 @@ export const DFUWorkunitDetails: React.FunctionComponent<DFUWorkunitDetailsProps
     const [jobname, setJobname] = React.useState("");
     const [_protected, setProtected] = React.useState(false);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToDeleteThisWorkunit,
+        onSubmit: React.useCallback(() => {
+            workunit?.doDelete().then(() => replaceUrl("/dfuworkunits"));
+        }, [workunit])
+    });
+
     React.useEffect(() => {
         setWorkunit(ESPDFUWorkunit.Get(wuid));
         FileSpray.GetDFUWorkunit({ request: { wuid } }).then(response => {
@@ -81,96 +90,91 @@ export const DFUWorkunitDetails: React.FunctionComponent<DFUWorkunitDetailsProps
         },
         {
             key: "delete", text: nlsHPCC.Delete, iconProps: { iconName: "Delete" }, disabled: canDelete,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToDeleteThisWorkunit)) {
-                    workunit?.doDelete();
-                    pushUrl("/dfuworkunits");
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "abort", text: nlsHPCC.Abort, disabled: canAbort,
             onClick: () => workunit?.abort()
         },
-    ], [_protected, canAbort, canDelete, canSave, jobname, workunit, wuid]);
-
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-    ], []);
+    ], [_protected, canAbort, canDelete, canSave, jobname, setShowDeleteConfirm, workunit, wuid]);
 
-    return <SizeMe monitorHeight>{({ size }) =>
-        <Pivot
-            overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
-            onLinkClick={evt => {
-                if (evt.props.itemKey === "target") {
-                    pushUrl(`/files/${dfuWuData?.DestGroupName}/${dfuWuData?.DestLogicalName}`);
-                } else {
-                    pushUrl(`/dfuworkunits/${wuid}/${evt.props.itemKey}`);
-                }
-            }}
-        >
-            <PivotItem headerText={wuid} itemKey="summary" style={pivotItemStyle(size)} >
-                <Sticky stickyPosition={StickyPositionType.Header}>
-                    <CommandBar items={buttons} farItems={rightButtons} />
-                </Sticky>
-                <TableGroup fields={{
-                    "id": { label: nlsHPCC.ID, type: "string", value: wuid, readonly: true },
-                    "clusterName": { label: nlsHPCC.ClusterName, type: "string", value: dfuWuData?.ClusterName, readonly: true },
-                    "jobname": { label: nlsHPCC.JobName, type: "string", value: jobname },
-                    "dfuServerName": { label: nlsHPCC.DFUServerName, type: "string", value: dfuWuData?.DFUServerName, readonly: true },
-                    "queue": { label: nlsHPCC.Queue, type: "string", value: dfuWuData?.Queue, readonly: true },
-                    "user": { label: nlsHPCC.User, type: "string", value: dfuWuData?.Owner, readonly: true },
-                    "protected": { label: nlsHPCC.Protected, type: "checkbox", value: _protected },
-                    "command": { label: nlsHPCC.Command, type: "string", value: FileSpray.CommandMessages[dfuWuData?.Command], readonly: true },
-                    "state": { label: nlsHPCC.State, type: "string", value: FileSpray.States[dfuWuData?.State], readonly: true },
-                    "timeStarted": { label: nlsHPCC.TimeStarted, type: "string", value: dfuWuData?.TimeStarted, readonly: true },
-                    "timeStopped": { label: nlsHPCC.TimeStopped, type: "string", value: dfuWuData?.TimeStopped, readonly: true },
-                    "percentDone": { label: nlsHPCC.PercentDone, type: "progress", value: dfuWuData?.PercentDone, readonly: true },
-                    "progressMessage": { label: nlsHPCC.ProgressMessage, type: "string", value: dfuWuData?.ProgressMessage, readonly: true },
-                    "summaryMessage": { label: nlsHPCC.SummaryMessage, type: "string", value: dfuWuData?.SummaryMessage, readonly: true },
-                }} onChange={(id, value) => {
-                    switch (id) {
-                        case "jobname":
-                            setJobname(value);
-                            break;
-                        case "protected":
-                            setProtected(value);
-                            break;
-                        default:
-                            logger.debug(`${id}:  ${value}`);
+    return <>
+        <SizeMe monitorHeight>{({ size }) =>
+            <Pivot
+                overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
+                onLinkClick={evt => {
+                    if (evt.props.itemKey === "target") {
+                        pushUrl(`/files/${dfuWuData?.DestGroupName}/${dfuWuData?.DestLogicalName}`);
+                    } else {
+                        pushUrl(`/dfuworkunits/${wuid}/${evt.props.itemKey}`);
                     }
-                }} />
-                <hr />
-                <h2>{nlsHPCC.Source} ({nlsHPCC.Fixed})</h2>
-                <TableGroup fields={{
-                    "ip": { label: nlsHPCC.IP, type: "string", value: dfuWuData?.SourceIP, readonly: true },
-                    "directory": { label: nlsHPCC.Directory, type: "string", value: dfuWuData?.SourceDirectory, readonly: true },
-                    "filePath": { label: nlsHPCC.FilePath, type: "string", value: dfuWuData?.SourceFilePath, readonly: true },
-                    "numParts": { label: nlsHPCC.NumberofParts, type: "string", value: dfuWuData?.SourceNumParts, readonly: true },
-                    "format": { label: nlsHPCC.Format, type: "string", value: FileSpray.FormatMessages[dfuWuData?.SourceFormat], readonly: true },
-                    "recordSize": { label: nlsHPCC.RecordSize, type: "string", value: dfuWuData?.SourceRecordSize, readonly: true },
-                }} />
-                <hr />
-                <h2>{nlsHPCC.Target}</h2>
-                <TableGroup fields={{
-                    "directory": { label: nlsHPCC.Directory, type: "string", value: dfuWuData?.DestDirectory, readonly: true },
-                    "logicalName": { label: nlsHPCC.LogicalName, type: "string", value: dfuWuData?.DestLogicalName, readonly: true },
-                    "groupName": { label: nlsHPCC.GroupName, type: "string", value: dfuWuData?.DestGroupName, readonly: true },
-                    "numParts": { label: nlsHPCC.NumberofParts, type: "string", value: dfuWuData?.DestNumParts, readonly: true },
-                }} />
-                <hr />
-                <h2>{nlsHPCC.Other}</h2>
-                <TableGroup fields={{
-                    "monitorSub": { label: nlsHPCC.MonitorSub, type: "string", value: dfuWuData?.MonitorSub ? "true" : "false", readonly: true },
-                    "overwrite": { label: nlsHPCC.Overwrite, type: "string", value: dfuWuData?.Overwrite ? "true" : "false", readonly: true },
-                    "replicate": { label: nlsHPCC.Replicate, type: "string", value: dfuWuData?.Replicate ? "true" : "false", readonly: true },
-                    "compress": { label: nlsHPCC.Compress, type: "string", value: dfuWuData?.Compress ? "true" : "false", readonly: true },
-                }} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.XML} itemKey="xml" style={pivotItemStyle(size, 0)}>
-                <XMLSourceEditor text={wuXML} readonly={true} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.Target} itemKey="target"></PivotItem>
-        </Pivot>
-    }</SizeMe>;
+                }}
+            >
+                <PivotItem headerText={wuid} itemKey="summary" style={pivotItemStyle(size)} >
+                    <Sticky stickyPosition={StickyPositionType.Header}>
+                        <CommandBar items={buttons} />
+                    </Sticky>
+                    <TableGroup fields={{
+                        "id": { label: nlsHPCC.ID, type: "string", value: wuid, readonly: true },
+                        "clusterName": { label: nlsHPCC.ClusterName, type: "string", value: dfuWuData?.ClusterName, readonly: true },
+                        "jobname": { label: nlsHPCC.JobName, type: "string", value: jobname },
+                        "dfuServerName": { label: nlsHPCC.DFUServerName, type: "string", value: dfuWuData?.DFUServerName, readonly: true },
+                        "queue": { label: nlsHPCC.Queue, type: "string", value: dfuWuData?.Queue, readonly: true },
+                        "user": { label: nlsHPCC.User, type: "string", value: dfuWuData?.Owner, readonly: true },
+                        "protected": { label: nlsHPCC.Protected, type: "checkbox", value: _protected },
+                        "command": { label: nlsHPCC.Command, type: "string", value: FileSpray.CommandMessages[dfuWuData?.Command], readonly: true },
+                        "state": { label: nlsHPCC.State, type: "string", value: FileSpray.States[dfuWuData?.State], readonly: true },
+                        "timeStarted": { label: nlsHPCC.TimeStarted, type: "string", value: dfuWuData?.TimeStarted, readonly: true },
+                        "timeStopped": { label: nlsHPCC.TimeStopped, type: "string", value: dfuWuData?.TimeStopped, readonly: true },
+                        "percentDone": { label: nlsHPCC.PercentDone, type: "progress", value: dfuWuData?.PercentDone, readonly: true },
+                        "progressMessage": { label: nlsHPCC.ProgressMessage, type: "string", value: dfuWuData?.ProgressMessage, readonly: true },
+                        "summaryMessage": { label: nlsHPCC.SummaryMessage, type: "string", value: dfuWuData?.SummaryMessage, readonly: true },
+                    }} onChange={(id, value) => {
+                        switch (id) {
+                            case "jobname":
+                                setJobname(value);
+                                break;
+                            case "protected":
+                                setProtected(value);
+                                break;
+                            default:
+                                logger.debug(`${id}:  ${value}`);
+                        }
+                    }} />
+                    <hr />
+                    <h2>{nlsHPCC.Source} ({nlsHPCC.Fixed})</h2>
+                    <TableGroup fields={{
+                        "ip": { label: nlsHPCC.IP, type: "string", value: dfuWuData?.SourceIP, readonly: true },
+                        "directory": { label: nlsHPCC.Directory, type: "string", value: dfuWuData?.SourceDirectory, readonly: true },
+                        "filePath": { label: nlsHPCC.FilePath, type: "string", value: dfuWuData?.SourceFilePath, readonly: true },
+                        "numParts": { label: nlsHPCC.NumberofParts, type: "string", value: dfuWuData?.SourceNumParts, readonly: true },
+                        "format": { label: nlsHPCC.Format, type: "string", value: FileSpray.FormatMessages[dfuWuData?.SourceFormat], readonly: true },
+                        "recordSize": { label: nlsHPCC.RecordSize, type: "string", value: dfuWuData?.SourceRecordSize, readonly: true },
+                    }} />
+                    <hr />
+                    <h2>{nlsHPCC.Target}</h2>
+                    <TableGroup fields={{
+                        "directory": { label: nlsHPCC.Directory, type: "string", value: dfuWuData?.DestDirectory, readonly: true },
+                        "logicalName": { label: nlsHPCC.LogicalName, type: "string", value: dfuWuData?.DestLogicalName, readonly: true },
+                        "groupName": { label: nlsHPCC.GroupName, type: "string", value: dfuWuData?.DestGroupName, readonly: true },
+                        "numParts": { label: nlsHPCC.NumberofParts, type: "string", value: dfuWuData?.DestNumParts, readonly: true },
+                    }} />
+                    <hr />
+                    <h2>{nlsHPCC.Other}</h2>
+                    <TableGroup fields={{
+                        "monitorSub": { label: nlsHPCC.MonitorSub, type: "string", value: dfuWuData?.MonitorSub ? "true" : "false", readonly: true },
+                        "overwrite": { label: nlsHPCC.Overwrite, type: "string", value: dfuWuData?.Overwrite ? "true" : "false", readonly: true },
+                        "replicate": { label: nlsHPCC.Replicate, type: "string", value: dfuWuData?.Replicate ? "true" : "false", readonly: true },
+                        "compress": { label: nlsHPCC.Compress, type: "string", value: dfuWuData?.Compress ? "true" : "false", readonly: true },
+                    }} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.XML} itemKey="xml" style={pivotItemStyle(size, 0)}>
+                    <XMLSourceEditor text={wuXML} readonly={true} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.Target} itemKey="target"></PivotItem>
+            </Pivot>
+        }</SizeMe>
+        <DeleteConfirm />
+    </>;
 };

+ 12 - 7
esp/src/src-react/components/DFUWorkunits.tsx

@@ -5,6 +5,7 @@ import * as ESPDFUWorkunit from "src/ESPDFUWorkunit";
 import * as FileSpray from "src/FileSpray";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
@@ -112,6 +113,14 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedWorkunits + "\n\n" + selection.map(s => s.Wuid).join("\n"),
+        onSubmit: React.useCallback(() => {
+            FileSpray.DFUWorkunitsAction(selection, nlsHPCC.Delete).then(() => refreshTable(true));
+        }, [refreshTable, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -133,12 +142,7 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasNotProtected, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const list = selection.map(s => s.Wuid);
-                if (confirm(nlsHPCC.DeleteSelectedWorkunits + "\n" + list)) {
-                    FileSpray.DFUWorkunitsAction(selection, nlsHPCC.Delete).then(() => refreshTable(true));
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -167,7 +171,7 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
                 setMine(!mine);
             }
         },
-    ], [mine, refreshTable, selection, store, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
+    ], [mine, refreshTable, selection, setShowDeleteConfirm, store, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -201,6 +205,7 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
             <>
                 <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
+                <DeleteConfirm />
             </>
         }
     />;

+ 13 - 7
esp/src/src-react/components/FileHistory.tsx

@@ -3,6 +3,7 @@ import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluen
 import { useConst } from "@fluentui/react-hooks";
 import { Memory, Observable } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useFileHistory } from "../hooks/file";
 import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
@@ -37,6 +38,12 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.EraseHistory,
+        message: nlsHPCC.EraseHistoryQ + "\n" + logicalFile + "?",
+        onSubmit: eraseHistory
+    });
+
     React.useEffect(() => {
         store.setData(history);
         refreshTable();
@@ -51,18 +58,17 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "erase", text: nlsHPCC.EraseHistory, disabled: history?.length === 0,
-            onClick: () => {
-                if (confirm(nlsHPCC.EraseHistoryQ + "\n" + logicalFile + "?")) {
-                    eraseHistory();
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
-    ], [eraseHistory, history?.length, logicalFile, refreshData]);
+    ], [history?.length, refreshData, setShowDeleteConfirm]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <Grid />
+            <>
+                <Grid />
+                <DeleteConfirm />
+            </>
         }
     />;
 };

+ 18 - 11
esp/src/src-react/components/FileSummary.tsx

@@ -5,6 +5,7 @@ import { formatCost } from "src/Session";
 import * as WsDfu from "src/WsDfu";
 import * as Utility from "src/Utility";
 import { getStateImageName, IFile } from "src/ESPLogicalFile";
+import { useConfirm } from "../hooks/confirm";
 import { useFile } from "../hooks/file";
 import { useBuildInfo } from "../hooks/platform";
 import { ShortVerticalDivider } from "./Common";
@@ -13,6 +14,7 @@ import { CopyFile } from "./forms/CopyFile";
 import { DesprayFile } from "./forms/DesprayFile";
 import { RenameFile } from "./forms/RenameFile";
 import { ReplicateFile } from "./forms/ReplicateFile";
+import { replaceUrl } from "../util/history";
 
 import "react-reflex/styles.css";
 
@@ -40,6 +42,19 @@ export const FileSummary: React.FunctionComponent<FileSummaryProps> = ({
     const [showDesprayFile, setShowDesprayFile] = React.useState(false);
     const [showReplicateFile, setShowReplicateFile] = React.useState(false);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToDeleteThisFile,
+        onSubmit: React.useCallback(() => {
+            WsDfu.DFUArrayAction([file], "Delete").then(response => {
+                const actionInfo = response?.DFUArrayActionResponse?.ActionResults?.DFUActionInfo;
+                if (actionInfo && actionInfo.length && !actionInfo[0].Failed) {
+                    replaceUrl("/files");
+                }
+            });
+        }, [file])
+    });
+
     const isDFUWorkunit = React.useMemo(() => {
         return file?.Wuid?.length && file?.Wuid[0] === "D";
     }, [file?.Wuid]);
@@ -97,16 +112,7 @@ export const FileSummary: React.FunctionComponent<FileSummaryProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, iconProps: { iconName: "Delete" }, disabled: !file,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToDeleteThisFile)) {
-                    WsDfu.DFUArrayAction([file], "Delete").then(response => {
-                        const actionInfo = response?.DFUArrayActionResponse?.ActionResults?.DFUActionInfo;
-                        if (actionInfo && actionInfo.length && !actionInfo[0].Failed) {
-                            window.history.back();
-                        }
-                    });
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -126,7 +132,7 @@ export const FileSummary: React.FunctionComponent<FileSummaryProps> = ({
             key: "replicate", text: nlsHPCC.Replicate, disabled: !canReplicateFlag || !replicateFlag,
             onClick: () => setShowReplicateFile(true)
         },
-    ], [_protected, canReplicateFlag, canSave, description, file, logicalFile, refresh, replicateFlag, restricted]);
+    ], [_protected, canReplicateFlag, canSave, description, file, logicalFile, refresh, replicateFlag, restricted, setShowDeleteConfirm]);
 
     const protectedImage = _protected ? Utility.getImageURL("locked.png") : Utility.getImageURL("unlocked.png");
     const stateImage = Utility.getImageURL(getStateImageName(file as unknown as IFile));
@@ -194,5 +200,6 @@ export const FileSummary: React.FunctionComponent<FileSummaryProps> = ({
         <DesprayFile cluster={cluster} logicalFile={logicalFile} showForm={showDesprayFile} setShowForm={setShowDesprayFile} />
         <RenameFile cluster={cluster} logicalFile={logicalFile} showForm={showRenameFile} setShowForm={setShowRenameFile} />
         <ReplicateFile cluster={cluster} logicalFile={logicalFile} showForm={showReplicateFile} setShowForm={setShowReplicateFile} />
+        <DeleteConfirm />
     </>;
 };

+ 34 - 28
esp/src/src-react/components/GroupMembers.tsx

@@ -7,6 +7,7 @@ import { Memory } from "src/Memory";
 import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
 import { ShortVerticalDivider } from "./Common";
+import { useConfirm } from "../hooks/confirm";
 import { pushUrl } from "../util/history";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { DojoGrid, selector } from "./DojoGrid";
@@ -49,17 +50,6 @@ export const GroupMembers: React.FunctionComponent<GroupMembersProps> = ({
         passwordexpiration: { label: nlsHPCC.PasswordExpiration }
     });
 
-    //  Selection  ---
-    React.useEffect(() => {
-        const state = { ...defaultUIState };
-
-        if (selection.length > 0) {
-            state.hasSelection = true;
-        }
-
-        setUIState(state);
-    }, [selection]);
-
     const refreshTable = React.useCallback((clearSelection = false) => {
         WsAccess.GroupMemberQuery({
             request: { GroupName: groupname }
@@ -81,6 +71,36 @@ export const GroupMembers: React.FunctionComponent<GroupMembersProps> = ({
             ;
     }, [grid, gridQuery, gridStore, groupname]);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToRemoveUserFrom,
+        onSubmit: React.useCallback(() => {
+            const requests = [];
+            selection.forEach((user, idx) => {
+                const request = {
+                    groupname: groupname,
+                    action: "Delete"
+                };
+                request["usernames_i" + idx] = user.username;
+                requests.push(WsAccess.GroupMemberEdit({ request: request }));
+            });
+            Promise.all(requests)
+                .then(responses => refreshTable())
+                .catch(logger.error);
+        }, [groupname, refreshTable, selection])
+    });
+
+    //  Selection  ---
+    React.useEffect(() => {
+        const state = { ...defaultUIState };
+
+        if (selection.length > 0) {
+            state.hasSelection = true;
+        }
+
+        setUIState(state);
+    }, [selection]);
+
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
@@ -106,24 +126,9 @@ export const GroupMembers: React.FunctionComponent<GroupMembersProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToRemoveUserFrom)) {
-                    const requests = [];
-                    selection.forEach((user, idx) => {
-                        const request = {
-                            groupname: groupname,
-                            action: "Delete"
-                        };
-                        request["usernames_i" + idx] = user.username;
-                        requests.push(WsAccess.GroupMemberEdit({ request: request }));
-                    });
-                    Promise.all(requests)
-                        .then(responses => refreshTable())
-                        .catch(logger.error);
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
-    ], [groupname, refreshTable, selection, uiState.hasSelection]);
+    ], [refreshTable, selection, setShowDeleteConfirm, uiState.hasSelection]);
 
     React.useEffect(() => {
         if (!grid || !gridStore) return;
@@ -141,6 +146,7 @@ export const GroupMembers: React.FunctionComponent<GroupMembersProps> = ({
             }
         />
         <GroupAddUserForm showForm={showAdd} setShowForm={setShowAdd} refreshGrid={refreshTable} groupname={groupname} />
+        <DeleteConfirm />
     </>;
 
 };

+ 31 - 26
esp/src/src-react/components/Groups.tsx

@@ -6,6 +6,7 @@ import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
 import { ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector } from "./DojoGrid";
+import { useConfirm } from "../hooks/confirm";
 import { AddGroupForm } from "./forms/AddGroup";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushUrl } from "../util/history";
@@ -44,6 +45,33 @@ export const Groups: React.FunctionComponent<GroupsProps> = ({
         groupDesc: { label: nlsHPCC.Description }
     });
 
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", {});
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid]);
+
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedGroups + "\n" + selection.map(group => group.name).join("\n"),
+        onSubmit: React.useCallback(() => {
+            const request = { ActionType: "delete" };
+            selection.forEach((item, idx) => {
+                request["groupnames_i" + idx] = item.name;
+            });
+
+            WsAccess.GroupAction({
+                request: request
+            })
+                .then((response) => {
+                    refreshTable(true);
+                })
+                .catch(logger.error)
+                ;
+        }, [refreshTable, selection])
+    });
+
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -55,13 +83,6 @@ export const Groups: React.FunctionComponent<GroupsProps> = ({
         setUIState(state);
     }, [selection]);
 
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [grid]);
-
     const exportGroups = React.useCallback(() => {
         let groupnames = "";
         selection.forEach((item, idx) => {
@@ -98,31 +119,14 @@ export const Groups: React.FunctionComponent<GroupsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(group => group.name).join("\n");
-                if (confirm(nlsHPCC.DeleteSelectedGroups + "\n" + list)) {
-                    const request = { ActionType: "delete" };
-                    selection.forEach((item, idx) => {
-                        request["groupnames_i" + idx] = item.name;
-                    });
-
-                    WsAccess.GroupAction({
-                        request: request
-                    })
-                        .then((response) => {
-                            refreshTable(true);
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            },
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "export", text: nlsHPCC.Export,
             onClick: () => exportGroups()
         },
-    ], [exportGroups, refreshTable, selection, uiState]);
+    ], [exportGroups, refreshTable, selection, setShowDeleteConfirm, uiState]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} />}
@@ -133,6 +137,7 @@ export const Groups: React.FunctionComponent<GroupsProps> = ({
                     columns={gridColumns} setGrid={setGrid} setSelection={setSelection}
                 />
                 <AddGroupForm showForm={showAddGroup} setShowForm={setShowAddGroup} refreshGrid={refreshTable} />
+                <DeleteConfirm />
             </>
         }
     />;

+ 29 - 24
esp/src/src-react/components/LandingZone.tsx

@@ -9,6 +9,7 @@ import * as FileSpray from "src/FileSpray";
 import * as ESPRequest from "src/ESPRequest";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
@@ -159,6 +160,31 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedFiles + "\n" + selection.map(s => s.name).join("\n"),
+        onSubmit: React.useCallback(() => {
+            selection.forEach((item, idx) => {
+                if (item._isUserFile) {
+                    store.removeUserFile(item);
+                    refreshTable(true);
+                } else {
+                    FileSpray.DeleteDropZoneFile({
+                        request: {
+                            NetAddress: item.NetAddress,
+                            Path: item.fullFolderPath,
+                            OS: item.OS,
+                            Names: item.name
+                        },
+                        load: function (response) {
+                            refreshTable(true);
+                        }
+                    });
+                }
+            });
+        }, [refreshTable, selection, store])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -194,29 +220,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !selection.length, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const list = selection.map(s => s.name);
-                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
-                    selection.forEach((item, idx) => {
-                        if (item._isUserFile) {
-                            store.removeUserFile(item);
-                            refreshTable(true);
-                        } else {
-                            FileSpray.DeleteDropZoneFile({
-                                request: {
-                                    NetAddress: item.NetAddress,
-                                    Path: item.fullFolderPath,
-                                    OS: item.OS,
-                                    Names: item.name
-                                },
-                                load: function (response) {
-                                    refreshTable(true);
-                                }
-                            });
-                        }
-                    });
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -254,7 +258,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
             onClick: () => setShowBlob(true)
         },
         { key: "divider_6", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> }
-    ], [store, refreshTable, selection]);
+    ], [refreshTable, selection, setShowDeleteConfirm]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -379,6 +383,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
                     formMinWidth={620} selection={selection}
                     showForm={showBlob} setShowForm={setShowBlob}
                 />
+                <DeleteConfirm />
             </>
         }
     />;

+ 54 - 44
esp/src/src-react/components/PackageMapDetails.tsx

@@ -7,6 +7,7 @@ import * as WsPackageMaps from "src/WsPackageMaps";
 import { pivotItemStyle } from "../layouts/pivot";
 import { pushUrl } from "../util/history";
 import { PackageMapParts } from "./PackageMapParts";
+import { useConfirm } from "../hooks/confirm";
 import { TableGroup } from "./forms/Groups";
 import { XMLSourceEditor } from "./SourceEditor";
 
@@ -26,6 +27,29 @@ export const PackageMapDetails: React.FunctionComponent<PackageMapDetailsProps>
     const [isActive, setIsActive] = React.useState(false);
     const [xml, setXml] = React.useState("");
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteThisPackage,
+        onSubmit: React.useCallback(() => {
+            WsPackageMaps.deletePackageMap({
+                request: {
+                    Target: _package?.Target,
+                    Process: _package?.Process,
+                    PackageMap: _package?.Id
+                }
+            })
+                .then(({ DeletePackageResponse, Exceptions }) => {
+                    if (DeletePackageResponse?.status?.Code === 0) {
+                        pushUrl("/packagemaps");
+                    } else if (Exceptions) {
+                        logger.error(Exceptions.Exception[0].Message);
+                    }
+                })
+                .catch(logger.error)
+                ;
+        }, [_package])
+    });
+
     React.useEffect(() => {
         WsPackageMaps.PackageMapQuery({})
             .then(({ ListPackagesResponse }) => {
@@ -85,51 +109,37 @@ export const PackageMapDetails: React.FunctionComponent<PackageMapDetailsProps>
         {
             key: "delete", text: nlsHPCC.Delete,
             onClick: () => {
-                if (confirm(nlsHPCC.DeleteThisPackage)) {
-                    WsPackageMaps.deletePackageMap({
-                        request: {
-                            Target: _package?.Target,
-                            Process: _package?.Process,
-                            PackageMap: _package?.Id
-                        }
-                    })
-                        .then(({ DeletePackageResponse, Exceptions }) => {
-                            if (DeletePackageResponse?.status?.Code === 0) {
-                                pushUrl("/packagemaps");
-                            } else if (Exceptions) {
-                                logger.error(Exceptions.Exception[0].Message);
-                            }
-                        })
-                        .catch(logger.error)
-                        ;
-                }
+                setShowDeleteConfirm(true);
             }
         },
-    ], [_package, isActive]);
+    ], [_package, isActive, setShowDeleteConfirm]);
 
-    return <SizeMe monitorHeight>{({ size }) =>
-        <Pivot
-            overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
-            onLinkClick={evt => {
-                pushUrl(`/packagemaps/${name}/${evt.props.itemKey}`);
-            }}
-        >
-            <PivotItem headerText={name} itemKey="summary" style={pivotItemStyle(size)} >
-                <Sticky stickyPosition={StickyPositionType.Header}>
-                    <CommandBar items={buttons} />
-                </Sticky>
-                <TableGroup fields={{
-                    "target": { label: nlsHPCC.ID, type: "string", value: _package?.Id, readonly: true },
-                    "process": { label: nlsHPCC.ClusterName, type: "string", value: _package?.Process, readonly: true },
-                    "active": { label: nlsHPCC.Active, type: "string", value: isActive ? "true" : "false", readonly: true },
-                }} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.XML} itemKey="xml" style={pivotItemStyle(size, 0)}>
-                <XMLSourceEditor text={xml} readonly={true} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.title_PackageParts} itemKey="parts" style={pivotItemStyle(size, 0)}>
-                <PackageMapParts name={name} />
-            </PivotItem>
-        </Pivot>
-    }</SizeMe>;
+    return <>
+        <SizeMe monitorHeight>{({ size }) =>
+            <Pivot
+                overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
+                onLinkClick={evt => {
+                    pushUrl(`/packagemaps/${name}/${evt.props.itemKey}`);
+                }}
+            >
+                <PivotItem headerText={name} itemKey="summary" style={pivotItemStyle(size)} >
+                    <Sticky stickyPosition={StickyPositionType.Header}>
+                        <CommandBar items={buttons} />
+                    </Sticky>
+                    <TableGroup fields={{
+                        "target": { label: nlsHPCC.ID, type: "string", value: _package?.Id, readonly: true },
+                        "process": { label: nlsHPCC.ClusterName, type: "string", value: _package?.Process, readonly: true },
+                        "active": { label: nlsHPCC.Active, type: "string", value: isActive ? "true" : "false", readonly: true },
+                    }} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.XML} itemKey="xml" style={pivotItemStyle(size, 0)}>
+                    <XMLSourceEditor text={xml} readonly={true} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.title_PackageParts} itemKey="parts" style={pivotItemStyle(size, 0)}>
+                    <PackageMapParts name={name} />
+                </PivotItem>
+            </Pivot>
+        }</SizeMe>
+        <DeleteConfirm />
+    </>;
 };

+ 31 - 25
esp/src/src-react/components/PackageMapParts.tsx

@@ -8,6 +8,7 @@ import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import * as WsPackageMaps from "src/WsPackageMaps";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { pushUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
@@ -53,6 +54,33 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToDeleteThisPart,
+        onSubmit: React.useCallback(() => {
+            selection.forEach((item, idx) => {
+                WsPackageMaps.RemovePartFromPackageMap({
+                    request: {
+                        PackageMap: name.split("::")[1],
+                        Target: _package.Target,
+                        PartName: item.Part
+                    }
+                })
+                    .then(({ RemovePartFromPackageMapResponse, Exceptions }) => {
+                        if (RemovePartFromPackageMapResponse?.status?.Code === 0) {
+                            store.remove(item.Part);
+                            refreshTable();
+                        } else if (Exceptions?.Exception.length > 0) {
+                            setShowError(true);
+                            setErrorMessage(Exceptions?.Exception[0].Message);
+                        }
+                    })
+                    .catch(logger.error)
+                    ;
+            });
+        }, [_package.Target, name, refreshTable, selection, store])
+    });
+
     //  Command Bar ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -66,30 +94,7 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.RemovePart, disabled: !uiState.hasSelection,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToDeleteThisPart)) {
-                    selection.forEach((item, idx) => {
-                        WsPackageMaps.RemovePartFromPackageMap({
-                            request: {
-                                PackageMap: name.split("::")[1],
-                                Target: _package.Target,
-                                PartName: item.Part
-                            }
-                        })
-                            .then(({ RemovePartFromPackageMapResponse, Exceptions }) => {
-                                if (RemovePartFromPackageMapResponse?.status?.Code === 0) {
-                                    store.remove(item.Part);
-                                    refreshTable();
-                                } else if (Exceptions?.Exception.length > 0) {
-                                    setShowError(true);
-                                    setErrorMessage(Exceptions?.Exception[0].Message);
-                                }
-                            })
-                            .catch(logger.error)
-                            ;
-                    });
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -104,7 +109,7 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
                 }
             }
         },
-    ], [_package, store, name, refreshTable, selection, uiState.hasSelection]);
+    ], [name, refreshTable, selection, setShowDeleteConfirm, uiState.hasSelection]);
 
     React.useEffect(() => {
         WsPackageMaps.getPackageMapById({ packageMap: name })
@@ -160,5 +165,6 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
             showForm={showAddPartForm} setShowForm={setShowAddPartForm} store={store}
             refreshTable={refreshTable} target={_package?.Target} packageMap={_package?.Id.split("::")[1]}
         />
+        <DeleteConfirm />
     </>;
 };

+ 30 - 24
esp/src/src-react/components/PackageMaps.tsx

@@ -5,6 +5,7 @@ import { SizeMe } from "react-sizeme";
 import * as ESPPackageProcess from "src/ESPPackageProcess";
 import * as WsPackageMaps from "src/WsPackageMaps";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { pivotItemStyle } from "../layouts/pivot";
 import { pushParams, pushUrl } from "../util/history";
@@ -210,6 +211,32 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedPackages,
+        onSubmit: React.useCallback(() => {
+            selection.forEach((item, idx) => {
+                WsPackageMaps.deletePackageMap({
+                    request: {
+                        PackageMap: item.Id,
+                        Target: item.Target,
+                        Process: item.Process
+                    }
+                })
+                    .then(({ DeletePackageResponse, Exceptions }) => {
+                        if (DeletePackageResponse?.status?.Code === 0) {
+                            refreshTable();
+                        } else if (Exceptions?.Exception.length > 0) {
+                            setShowError(true);
+                            setErrorMessage(Exceptions?.Exception[0].Message);
+                        }
+                    })
+                    .catch(logger.error)
+                    ;
+            });
+        }, [refreshTable, selection])
+    });
+
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
@@ -234,29 +261,7 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                if (confirm(nlsHPCC.DeleteSelectedPackages)) {
-                    selection.forEach((item, idx) => {
-                        WsPackageMaps.deletePackageMap({
-                            request: {
-                                PackageMap: item.Id,
-                                Target: item.Target,
-                                Process: item.Process
-                            }
-                        })
-                            .then(({ DeletePackageResponse, Exceptions }) => {
-                                if (DeletePackageResponse?.status?.Code === 0) {
-                                    refreshTable();
-                                } else if (Exceptions?.Exception.length > 0) {
-                                    setShowError(true);
-                                    setErrorMessage(Exceptions?.Exception[0].Message);
-                                }
-                            })
-                            .catch(logger.error)
-                            ;
-                    });
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -307,7 +312,7 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
                 setShowFilter(true);
             }
         },
-    ], [refreshTable, selection, store, uiState.hasSelection]);
+    ], [refreshTable, selection, setShowDeleteConfirm, store, uiState.hasSelection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -474,5 +479,6 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
             showForm={showAddForm} setShowForm={setShowAddForm}
             refreshTable={refreshTable} targets={targets} processes={processes}
         />
+        <DeleteConfirm />
     </>;
 };

+ 57 - 60
esp/src/src-react/components/Permissions.tsx

@@ -5,7 +5,7 @@ import { scopedLogger } from "@hpcc-js/util";
 import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
 import { ShortVerticalDivider } from "./Common";
-import { Confirm } from "./controls/Confirm";
+import { useConfirm } from "../hooks/confirm";
 import { DojoGrid, selector, tree } from "./DojoGrid";
 import { AddPermissionForm } from "./forms/AddPermission";
 import { HolyGrail } from "../layouts/HolyGrail";
@@ -31,11 +31,6 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
     const [grid, setGrid] = React.useState<any>(undefined);
     const [selection, setSelection] = React.useState([]);
 
-    const [showConfirm, setShowConfirm] = React.useState(false);
-    const [confirmTitle, setConfirmTitle] = React.useState("");
-    const [confirmMessage, setConfirmMessage] = React.useState("");
-    const [confirmOnSubmit, setConfirmOnSubmit] = React.useState(null);
-
     const [showAddPermission, setShowAddPermission] = React.useState(false);
     const [scopeScansEnabled, setScopeScansEnabled] = React.useState(false);
     const [modulesDn, setModulesDn] = React.useState("");
@@ -117,33 +112,55 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
         }
     }, [grid, gridQuery]);
 
-    const deletePermission = React.useCallback(() => {
-        const deleteRequests = {};
-        const requests = [];
-        selection.forEach((item, idx) => {
-            if (!deleteRequests[item.__hpcc_id]) {
-                deleteRequests[item.__hpcc_id] = {
-                    action: "Delete",
-                    BasednName: item.__hpcc_parent.name,
-                    rtype: item.__hpcc_parent.rtype,
-                    rtitle: item.__hpcc_parent.rtitle
-                };
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedPermissions + "\n\n" + selectedFileList,
+        onSubmit: React.useCallback(() => {
+            const deleteRequests = {};
+            const requests = [];
+            selection.forEach((item, idx) => {
+                if (!deleteRequests[item.__hpcc_id]) {
+                    deleteRequests[item.__hpcc_id] = {
+                        action: "Delete",
+                        BasednName: item.__hpcc_parent.name,
+                        rtype: item.__hpcc_parent.rtype,
+                        rtitle: item.__hpcc_parent.rtitle
+                    };
+                }
+                deleteRequests[item.__hpcc_id]["names_i" + idx] = item.name;
+            });
+            for (const key in deleteRequests) {
+                requests.push(WsAccess.ResourceDelete({
+                    request: deleteRequests[key]
+                }));
             }
-            deleteRequests[item.__hpcc_id]["names_i" + idx] = item.name;
-        });
-        for (const key in deleteRequests) {
-            requests.push(WsAccess.ResourceDelete({
-                request: deleteRequests[key]
-            }));
-        }
 
-        Promise.all(requests)
-            .then(() => {
-                refreshTable();
-            })
-            .catch(logger.error)
-            ;
-    }, [refreshTable, selection]);
+            Promise.all(requests)
+                .then(() => {
+                    refreshTable();
+                })
+                .catch(logger.error)
+                ;
+        }, [refreshTable, selection])
+    });
+
+    const [ClearPermissionsConfirm, setShowClearPermissionsConfirm] = useConfirm({
+        title: nlsHPCC.ClearPermissionsCache,
+        message: nlsHPCC.ClearPermissionsCacheConfirm,
+        onSubmit: () => WsAccess.ClearPermissionsCache
+    });
+
+    const [EnableScopesConfirm, setShowEnableScopesConfirm] = useConfirm({
+        title: nlsHPCC.EnableScopeScans,
+        message: nlsHPCC.EnableScopeScansConfirm,
+        onSubmit: () => WsAccess.EnableScopeScans
+    });
+
+    const [DisableScopesConfirm, setShowDisableScopesConfirm] = useConfirm({
+        title: nlsHPCC.DisableScopeScans,
+        message: nlsHPCC.DisableScopeScanConfirm,
+        onSubmit: () => WsAccess.DisableScopeScans
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -158,22 +175,12 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.canDelete,
-            onClick: () => {
-                setConfirmTitle(nlsHPCC.Delete);
-                setConfirmMessage(nlsHPCC.DeleteSelectedPermissions + "\n\n" + selectedFileList);
-                setConfirmOnSubmit(() => deletePermission);
-                setShowConfirm(true);
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "clearPermissions", text: nlsHPCC.ClearPermissionsCache,
-            onClick: () => {
-                setConfirmTitle(nlsHPCC.ClearPermissionsCache);
-                setConfirmMessage(nlsHPCC.ClearPermissionsCacheConfirm);
-                setConfirmOnSubmit(() => WsAccess.ClearPermissionsCache);
-                setShowConfirm(true);
-            }
+            onClick: () => setShowClearPermissionsConfirm(true)
         },
         {
             key: "advanced", text: nlsHPCC.Advanced, disabled: !uiState.hasSelection || !uiState.categorySelected,
@@ -182,23 +189,13 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
                     {
                         key: "enableScopeScans",
                         text: nlsHPCC.EnableScopeScans,
-                        onClick: () => {
-                            setConfirmTitle(nlsHPCC.EnableScopeScans);
-                            setConfirmMessage(nlsHPCC.EnableScopeScansConfirm);
-                            setConfirmOnSubmit(() => WsAccess.EnableScopeScans);
-                            setShowConfirm(true);
-                        },
+                        onClick: () => setShowEnableScopesConfirm(true),
                         disabled: scopeScansEnabled
                     },
                     {
                         key: "disableScopeScans",
                         text: nlsHPCC.DisableScopeScans,
-                        onClick: () => {
-                            setConfirmTitle(nlsHPCC.DisableScopeScans);
-                            setConfirmMessage(nlsHPCC.DisableScopeScanConfirm);
-                            setConfirmOnSubmit(() => WsAccess.DisableScopeScans);
-                            setShowConfirm(true);
-                        },
+                        onClick: () => setShowDisableScopesConfirm(true),
                         disabled: !scopeScansEnabled
                     },
                     { key: "fileScopeDefaults", text: nlsHPCC.FileScopeDefaultPermissions, onClick: (evt, item) => pushUrl("/security/permissions/_/File%20Scopes"), disabled: !uiState.fileScope },
@@ -209,7 +206,7 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
                 ],
             },
         },
-    ], [deletePermission, modulesDn, refreshTable, selectedFileList, scopeScansEnabled, uiState]);
+    ], [modulesDn, refreshTable, setShowClearPermissionsConfirm, setShowDeleteConfirm, setShowDisableScopesConfirm, setShowEnableScopesConfirm, scopeScansEnabled, uiState]);
 
     React.useEffect(() => {
         refreshTable();
@@ -220,10 +217,10 @@ export const Permissions: React.FunctionComponent<PermissionsProps> = ({
             header={<CommandBar items={buttons} overflowButtonProps={{}} />}
             main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />}
         />
-        <Confirm
-            show={showConfirm} setShow={setShowConfirm}
-            title={confirmTitle} message={confirmMessage} onSubmit={confirmOnSubmit}
-        />
+        <DeleteConfirm />
+        <ClearPermissionsConfirm />
+        <EnableScopesConfirm />
+        <DisableScopesConfirm />
         <AddPermissionForm showForm={showAddPermission} setShowForm={setShowAddPermission} refreshGrid={refreshTable} />
     </>;
 

+ 12 - 7
esp/src/src-react/components/Queries.tsx

@@ -4,6 +4,7 @@ import * as WsWorkunits from "src/WsWorkunits";
 import * as ESPQuery from "src/ESPQuery";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
@@ -170,6 +171,14 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedWorkunits + "\n" + selection.map(s => s.Id).join("\n"),
+        onSubmit: React.useCallback(() => {
+            WsWorkunits.WUQuerysetQueryAction(selection, "Delete").then(() => refreshTable(true));
+        }, [refreshTable, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -191,12 +200,7 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const list = selection.map(s => s.Id);
-                if (confirm(nlsHPCC.DeleteSelectedWorkunits + "\n" + list)) {
-                    WsWorkunits.WUQuerysetQueryAction(selection, "Delete").then(() => refreshTable(true));
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -228,7 +232,7 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
                 setMine(!mine);
             }
         },
-    ], [mine, refreshTable, selection, store, uiState.hasSelection, uiState.isActive, uiState.isNotActive, uiState.isNotSuspended, uiState.isSuspended, wuid]);
+    ], [mine, refreshTable, selection, setShowDeleteConfirm, store, uiState.hasSelection, uiState.isActive, uiState.isNotActive, uiState.isNotSuspended, uiState.isSuspended, wuid]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -263,6 +267,7 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
             <>
                 <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
+                <DeleteConfirm />
             </>
         }
     />;

+ 101 - 88
esp/src/src-react/components/QueryDetails.tsx

@@ -5,6 +5,7 @@ import { scopedLogger } from "@hpcc-js/util";
 import nlsHPCC from "src/nlsHPCC";
 import * as WsWorkunits from "src/WsWorkunits";
 import * as ESPQuery from "src/ESPQuery";
+import { useConfirm } from "../hooks/confirm";
 import { DojoAdapter } from "../layouts/DojoAdapter";
 import { pivotItemStyle } from "../layouts/pivot";
 import { pushUrl } from "../util/history";
@@ -36,6 +37,26 @@ export const QueryDetails: React.FunctionComponent<QueryDetailsProps> = ({
     const [suspended, setSuspended] = React.useState(false);
     const [activated, setActivated] = React.useState(false);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedWorkunits + "\n" + query?.QueryName,
+        onSubmit: React.useCallback(() => {
+            const selection = [{ QuerySetId: querySet, Id: queryId }];
+            WsWorkunits.WUQuerysetQueryAction(selection, "Delete")
+                .then(() => pushUrl("/queries"))
+                .catch(logger.error)
+                ;
+        }, [queryId, querySet])
+    });
+
+    const [ResetConfirm, setShowResetConfirm] = useConfirm({
+        title: nlsHPCC.Reset,
+        message: nlsHPCC.ResetThisQuery,
+        onSubmit: React.useCallback(() => {
+            query?.doReset().catch(logger.error);
+        }, [query])
+    });
+
     const canSave = query && (
         suspended !== query?.Suspended ||
         activated !== query?.Activated
@@ -67,25 +88,13 @@ export const QueryDetails: React.FunctionComponent<QueryDetailsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const selection = [{ QuerySetId: querySet, Id: queryId }];
-                if (confirm(nlsHPCC.DeleteSelectedWorkunits + "\n" + query?.QueryName)) {
-                    WsWorkunits.WUQuerysetQueryAction(selection, "Delete")
-                        .then(() => pushUrl("/queries"))
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
-            key: "reset", text: nlsHPCC.Reset, onClick: () => {
-                if (confirm(nlsHPCC.ResetThisQuery)) {
-                    query?.doReset().catch(logger.error);
-                }
-            }
+            key: "reset", text: nlsHPCC.Reset, onClick: () => setShowResetConfirm(true)
         },
-    ], [activated, canSave, query, queryId, querySet, suspended]);
+    ], [activated, canSave, query, queryId, querySet, setShowDeleteConfirm, setShowResetConfirm, suspended]);
 
     const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
     ], []);
@@ -101,78 +110,82 @@ export const QueryDetails: React.FunctionComponent<QueryDetailsProps> = ({
         });
     }, [setActivated, setSuspended, query]);
 
-    return <SizeMe monitorHeight>{({ size }) =>
-        <Pivot
-            overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
-            onLinkClick={evt => {
-                if (evt.props.itemKey === "workunit") {
-                    pushUrl(`/workunits/${query?.Wuid}`);
-                } else {
-                    pushUrl(`/queries/${querySet}/${queryId}/${evt.props.itemKey}`);
-                }
-            }}
-        >
-            <PivotItem headerText={queryId} itemKey="summary" style={pivotItemStyle(size)} >
-                <Sticky stickyPosition={StickyPositionType.Header}>
-                    <CommandBar items={buttons} farItems={rightButtons} />
-                </Sticky>
-                <TableGroup fields={{
-                    "name": { label: nlsHPCC.Name, type: "string", value: query?.QueryName, readonly: true },
-                    "querySet": { label: nlsHPCC.QuerySet, type: "string", value: query?.QuerySet, readonly: true },
-                    "priority": { label: nlsHPCC.Priority, type: "string", value: query?.Priority || "", readonly: true },
-                    "publishedBy": { label: nlsHPCC.PublishedBy, type: "string", value: query?.PublishedBy || "", readonly: true },
-                    "suspended": { label: nlsHPCC.Suspended, type: "checkbox", value: suspended },
-                    "suspendedBy": { label: nlsHPCC.SuspendedBy, type: "string", value: query?.SuspendedBy || "", readonly: true },
-                    "activated": { label: nlsHPCC.Activated, type: "checkbox", value: activated },
-                    "comment": { label: nlsHPCC.Comment, type: "string", value: query?.Comment || "", readonly: true },
-                }} onChange={(id, value) => {
-                    switch (id) {
-                        case "suspended":
-                            setSuspended(value);
-                            break;
-                        case "activated":
-                            setActivated(value);
-                            break;
-                        default:
-                            console.log(id, value);
+    return <>
+        <SizeMe monitorHeight>{({ size }) =>
+            <Pivot
+                overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab}
+                onLinkClick={evt => {
+                    if (evt.props.itemKey === "workunit") {
+                        pushUrl(`/workunits/${query?.Wuid}`);
+                    } else {
+                        pushUrl(`/queries/${querySet}/${queryId}/${evt.props.itemKey}`);
                     }
-                }} />
-                <hr />
-                <TableGroup fields={{
-                    "wuid": { label: nlsHPCC.WUID, type: "string", value: query?.Wuid, readonly: true },
-                    "dll": { label: nlsHPCC.Dll, type: "string", value: query?.Dll, readonly: true },
-                    "wuSnapShot": { label: nlsHPCC.WUSnapShot, type: "string", value: query?.WUSnapShot, readonly: true },
-                }} />
-                <hr />
-                <TableGroup fields={{
-                    "isLibrary": { label: nlsHPCC.IsLibrary, type: "string", value: query?.IsLibrary ? "true" : "false", readonly: true },
-                }} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.Errors} itemKey="errors" style={pivotItemStyle(size, 0)}>
-                <QueryErrors queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.LogicalFiles} itemKey="logicalFiles" itemCount={query?.LogicalFiles?.Item?.length || 0} style={pivotItemStyle(size, 0)}>
-                <QueryLogicalFiles queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.SuperFiles} itemKey="superfiles" itemCount={query?.SuperFiles?.SuperFile.length || 0} style={pivotItemStyle(size, 0)}>
-                <QuerySuperFiles queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.LibrariesUsed} itemKey="librariesUsed" itemCount={query?.LibrariesUsed?.Item?.length || 0} style={pivotItemStyle(size, 0)}>
-                <QueryLibrariesUsed queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.SummaryStatistics} itemKey="summaryStatistics" style={pivotItemStyle(size, 0)}>
-                <QuerySummaryStats queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.Graphs} itemKey="graphs" itemCount={query?.WUGraphs?.ECLGraph?.length || 0} style={pivotItemStyle(size, 0)}>
-                <QueryGraphs queryId={queryId} querySet={querySet} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.Resources} itemKey="resources" style={pivotItemStyle(size, 0)}>
-                <Resources wuid={query?.Wuid} />
-            </PivotItem>
-            <PivotItem headerText={nlsHPCC.TestPages} itemKey="testPages" style={pivotItemStyle(size, 0)}>
-                <DojoAdapter widgetClassID="QueryTestWidget" params={{ Id: queryId, QuerySetId: querySet }} />
-            </PivotItem>
-            <PivotItem headerText={query?.Wuid} itemKey="workunit"></PivotItem>
-        </Pivot>
-    }</SizeMe>;
+                }}
+            >
+                <PivotItem headerText={queryId} itemKey="summary" style={pivotItemStyle(size)} >
+                    <Sticky stickyPosition={StickyPositionType.Header}>
+                        <CommandBar items={buttons} farItems={rightButtons} />
+                    </Sticky>
+                    <TableGroup fields={{
+                        "name": { label: nlsHPCC.Name, type: "string", value: query?.QueryName, readonly: true },
+                        "querySet": { label: nlsHPCC.QuerySet, type: "string", value: query?.QuerySet, readonly: true },
+                        "priority": { label: nlsHPCC.Priority, type: "string", value: query?.Priority || "", readonly: true },
+                        "publishedBy": { label: nlsHPCC.PublishedBy, type: "string", value: query?.PublishedBy || "", readonly: true },
+                        "suspended": { label: nlsHPCC.Suspended, type: "checkbox", value: suspended },
+                        "suspendedBy": { label: nlsHPCC.SuspendedBy, type: "string", value: query?.SuspendedBy || "", readonly: true },
+                        "activated": { label: nlsHPCC.Activated, type: "checkbox", value: activated },
+                        "comment": { label: nlsHPCC.Comment, type: "string", value: query?.Comment || "", readonly: true },
+                    }} onChange={(id, value) => {
+                        switch (id) {
+                            case "suspended":
+                                setSuspended(value);
+                                break;
+                            case "activated":
+                                setActivated(value);
+                                break;
+                            default:
+                                console.log(id, value);
+                        }
+                    }} />
+                    <hr />
+                    <TableGroup fields={{
+                        "wuid": { label: nlsHPCC.WUID, type: "string", value: query?.Wuid, readonly: true },
+                        "dll": { label: nlsHPCC.Dll, type: "string", value: query?.Dll, readonly: true },
+                        "wuSnapShot": { label: nlsHPCC.WUSnapShot, type: "string", value: query?.WUSnapShot, readonly: true },
+                    }} />
+                    <hr />
+                    <TableGroup fields={{
+                        "isLibrary": { label: nlsHPCC.IsLibrary, type: "string", value: query?.IsLibrary ? "true" : "false", readonly: true },
+                    }} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.Errors} itemKey="errors" style={pivotItemStyle(size, 0)}>
+                    <QueryErrors queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.LogicalFiles} itemKey="logicalFiles" itemCount={query?.LogicalFiles?.Item?.length || 0} style={pivotItemStyle(size, 0)}>
+                    <QueryLogicalFiles queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.SuperFiles} itemKey="superfiles" itemCount={query?.SuperFiles?.SuperFile.length || 0} style={pivotItemStyle(size, 0)}>
+                    <QuerySuperFiles queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.LibrariesUsed} itemKey="librariesUsed" itemCount={query?.LibrariesUsed?.Item?.length || 0} style={pivotItemStyle(size, 0)}>
+                    <QueryLibrariesUsed queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.SummaryStatistics} itemKey="summaryStatistics" style={pivotItemStyle(size, 0)}>
+                    <QuerySummaryStats queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.Graphs} itemKey="graphs" itemCount={query?.WUGraphs?.ECLGraph?.length || 0} style={pivotItemStyle(size, 0)}>
+                    <QueryGraphs queryId={queryId} querySet={querySet} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.Resources} itemKey="resources" style={pivotItemStyle(size, 0)}>
+                    <Resources wuid={query?.Wuid} />
+                </PivotItem>
+                <PivotItem headerText={nlsHPCC.TestPages} itemKey="testPages" style={pivotItemStyle(size, 0)}>
+                    <DojoAdapter widgetClassID="QueryTestWidget" params={{ Id: queryId, QuerySetId: querySet }} />
+                </PivotItem>
+                <PivotItem headerText={query?.Wuid} itemKey="workunit"></PivotItem>
+            </Pivot>
+        }</SizeMe>
+        <DeleteConfirm />
+        <ResetConfirm />
+    </>;
 };

+ 23 - 17
esp/src/src-react/components/UserGroups.tsx

@@ -8,6 +8,7 @@ import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
 import { ShortVerticalDivider } from "./Common";
 import { pushUrl } from "../util/history";
+import { useConfirm } from "../hooks/confirm";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { DojoGrid, selector } from "./DojoGrid";
 import { UserAddGroupForm } from "./forms/UserAddGroup";
@@ -82,6 +83,25 @@ export const UserGroups: React.FunctionComponent<UserGroupsProps> = ({
             ;
     }, [grid, gridQuery, gridStore, username]);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToRemoveUserFrom,
+        onSubmit: React.useCallback(() => {
+            const requests = [];
+            selection.forEach((group, idx) => {
+                const request = {
+                    username: username,
+                    action: "Delete"
+                };
+                request["groupnames_i" + idx] = group.name;
+                requests.push(WsAccess.UserGroupEdit({ request: request }));
+            });
+            Promise.all(requests)
+                .then(responses => refreshTable())
+                .catch(logger.error);
+        }, [refreshTable, selection, username])
+    });
+
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
@@ -107,24 +127,9 @@ export const UserGroups: React.FunctionComponent<UserGroupsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToRemoveUserFrom)) {
-                    const requests = [];
-                    selection.forEach((group, idx) => {
-                        const request = {
-                            username: username,
-                            action: "Delete"
-                        };
-                        request["groupnames_i" + idx] = group.name;
-                        requests.push(WsAccess.UserGroupEdit({ request: request }));
-                    });
-                    Promise.all(requests)
-                        .then(responses => refreshTable())
-                        .catch(logger.error);
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
-    ], [refreshTable, selection, uiState.hasSelection, username]);
+    ], [refreshTable, selection, setShowDeleteConfirm, uiState.hasSelection]);
 
     React.useEffect(() => {
         if (!grid || !gridStore) return;
@@ -142,6 +147,7 @@ export const UserGroups: React.FunctionComponent<UserGroupsProps> = ({
             }
         />
         <UserAddGroupForm showForm={showAdd} setShowForm={setShowAdd} refreshGrid={refreshTable} username={username} />
+        <DeleteConfirm />
     </>;
 
 };

+ 23 - 18
esp/src/src-react/components/Users.tsx

@@ -3,6 +3,7 @@ import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluen
 import { scopedLogger } from "@hpcc-js/util";
 import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
@@ -68,6 +69,25 @@ export const Users: React.FunctionComponent<UsersProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedUsers + "\n" + selection.map(user => user.username).join("\n"),
+        onSubmit: React.useCallback(() => {
+            const request = {
+                ActionType: "delete"
+            };
+            selection.forEach((item, idx) => {
+                request["usernames_i" + idx] = item.username;
+            });
+            WsAccess.UserAction({ request: request })
+                .then(response => {
+                    refreshTable(true);
+                })
+                .catch(logger.error)
+                ;
+        }, [refreshTable, selection])
+    });
+
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -120,30 +140,14 @@ export const Users: React.FunctionComponent<UsersProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(user => user.username).join("\n");
-                if (confirm(nlsHPCC.DeleteSelectedUsers + "\n" + list)) {
-                    const request = {
-                        ActionType: "delete"
-                    };
-                    selection.forEach((item, idx) => {
-                        request["usernames_i" + idx] = item.username;
-                    });
-                    WsAccess.UserAction({ request: request })
-                        .then(response => {
-                            refreshTable(true);
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            },
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "export", text: nlsHPCC.Export,
             onClick: () => exportUsers()
         },
-    ], [exportUsers, refreshTable, selection, uiState]);
+    ], [exportUsers, refreshTable, selection, setShowDeleteConfirm, uiState]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -162,6 +166,7 @@ export const Users: React.FunctionComponent<UsersProps> = ({
             }
         />
         <AddUserForm showForm={showAddUser} setShowForm={setShowAddUser} refreshGrid={refreshTable} />
+        <DeleteConfirm />
     </>;
 
 };

+ 17 - 12
esp/src/src-react/components/WorkunitSummary.tsx

@@ -4,9 +4,10 @@ import { scopedLogger } from "@hpcc-js/util";
 import nlsHPCC from "src/nlsHPCC";
 import { WUStatus } from "src/react/index";
 import { formatCost } from "src/Session";
+import { useConfirm } from "../hooks/confirm";
 import { useWorkunit } from "../hooks/workunit";
 import { ReflexContainer, ReflexElement, ReflexSplitter, classNames, styles } from "../layouts/react-reflex";
-import { pushUrl } from "../util/history";
+import { pushUrl, replaceUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
 import { TableGroup } from "./forms/Groups";
 import { PublishQueryForm } from "./forms/PublishQuery";
@@ -54,6 +55,17 @@ export const WorkunitSummary: React.FunctionComponent<WorkunitSummaryProps> = ({
     const canDeschedule = workunit && workunit?.EventSchedule === 2;
     const canReschedule = workunit && workunit?.EventSchedule === 1;
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.YouAreAboutToDeleteThisWorkunit,
+        onSubmit: React.useCallback(() => {
+            workunit?.delete()
+                .then(response => replaceUrl("/workunits"))
+                .catch(logger.error)
+                ;
+        }, [workunit])
+    });
+
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
@@ -80,12 +92,7 @@ export const WorkunitSummary: React.FunctionComponent<WorkunitSummaryProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, iconProps: { iconName: "Delete" }, disabled: !canDelete,
-            onClick: () => {
-                if (confirm(nlsHPCC.YouAreAboutToDeleteThisWorkunit)) {
-                    workunit?.delete().catch(logger.error);
-                    pushUrl("/workunits");
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         {
             key: "restore", text: nlsHPCC.Restore, disabled: !workunit?.Archived,
@@ -144,10 +151,7 @@ export const WorkunitSummary: React.FunctionComponent<WorkunitSummaryProps> = ({
             key: "slaveLogs", text: nlsHPCC.SlaveLogs, disabled: !workunit?.ThorLogList,
             onClick: () => setShowThorSlaveLogs(true)
         },
-    ], [_protected, canDelete, canDeschedule, canReschedule, canSave, description, jobname, refresh, workunit, wuid]);
-
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-    ], []);
+    ], [_protected, canDelete, canDeschedule, canReschedule, canSave, description, jobname, refresh, setShowDeleteConfirm, workunit, wuid]);
 
     const serviceNames = React.useMemo(() => {
         return workunit?.ServiceNames?.Item?.join("\n") || "";
@@ -160,7 +164,7 @@ export const WorkunitSummary: React.FunctionComponent<WorkunitSummaryProps> = ({
                     <div className="pane-content">
                         <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
                             <Sticky stickyPosition={StickyPositionType.Header}>
-                                <CommandBar items={buttons} farItems={rightButtons} />
+                                <CommandBar items={buttons} />
                             </Sticky>
                             <Sticky stickyPosition={StickyPositionType.Header}>
                                 <WorkunitPersona wuid={wuid} />
@@ -212,5 +216,6 @@ export const WorkunitSummary: React.FunctionComponent<WorkunitSummaryProps> = ({
         <PublishQueryForm wuid={wuid} showForm={showPublishForm} setShowForm={setShowPublishForm} />
         <ZAPDialog wuid={wuid} showForm={showZapForm} setShowForm={setShowZapForm} />
         <SlaveLogs wuid={wuid} showForm={showThorSlaveLogs} setShowForm={setShowThorSlaveLogs} />
+        <DeleteConfirm />
     </>;
 };

+ 15 - 14
esp/src/src-react/components/Workunits.tsx

@@ -1,10 +1,12 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
+import { scopedLogger } from "@hpcc-js/util";
 import * as domClass from "dojo/dom-class";
 import * as WsWorkunits from "src/WsWorkunits";
 import * as ESPWorkunit from "src/ESPWorkunit";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
@@ -12,7 +14,6 @@ import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
-import { scopedLogger } from "@hpcc-js/util";
 
 const logger = scopedLogger("src-react/components/Workunits.tsx");
 
@@ -124,6 +125,14 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
         }
     });
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedWorkunits + "\n" + selection.map(s => s.Wuid).join("\n"),
+        onSubmit: React.useCallback(() => {
+            WsWorkunits.WUAction(selection, "Delete").then(() => refreshTable(true));
+        }, [refreshTable, selection])
+    });
+
     //  Filter  ---
     const filterFields: Fields = {};
     for (const fieldID in FilterFields) {
@@ -151,12 +160,7 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasNotProtected, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const list = selection.map(s => s.Wuid);
-                if (confirm(nlsHPCC.DeleteSelectedWorkunits + "\n" + list)) {
-                    WsWorkunits.WUAction(selection, "Delete").then(() => refreshTable(true));
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
@@ -179,17 +183,13 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
         { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
-            onClick: () => {
-                setShowFilter(true);
-            }
+            onClick: () => { setShowFilter(true); }
         },
         {
             key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
-            onClick: () => {
-                setMine(!mine);
-            }
+            onClick: () => { setMine(!mine); }
         },
-    ], [mine, refreshTable, selection, store, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
+    ], [mine, refreshTable, selection, setShowDeleteConfirm, store, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -226,6 +226,7 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
             <>
                 <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
+                <DeleteConfirm />
             </>
         }
     />;

+ 22 - 12
esp/src/src-react/components/XrefDirectories.tsx

@@ -6,6 +6,7 @@ import { HolyGrail } from "../layouts/HolyGrail";
 import * as WsDFUXref from "src/WsDFUXref";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import nlsHPCC from "src/nlsHPCC";
@@ -84,6 +85,19 @@ export const XrefDirectories: React.FunctionComponent<XrefDirectoriesProps> = ({
             ;
     }, [store, name, refreshTable]);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteDirectories,
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefCleanDirectories({ request: { Cluster: name } })
+                .then(response => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [name, refreshData])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -93,19 +107,10 @@ export const XrefDirectories: React.FunctionComponent<XrefDirectoriesProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "delete", text: nlsHPCC.DeleteEmptyDirectories,
-            onClick: () => {
-                if (confirm(nlsHPCC.DeleteDirectories)) {
-                    WsDFUXref.DFUXRefCleanDirectories({ request: { Cluster: name } })
-                        .then(response => {
-                            refreshData();
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ], [name, refreshData]);
+    ], [refreshData, setShowDeleteConfirm]);
 
     React.useEffect(() => {
         refreshData();
@@ -113,7 +118,12 @@ export const XrefDirectories: React.FunctionComponent<XrefDirectoriesProps> = ({
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={<Grid />}
+        main={
+            <>
+                <Grid />
+                <DeleteConfirm />
+            </>
+        }
     />;
 
 };

+ 37 - 24
esp/src/src-react/components/XrefFoundFiles.tsx

@@ -6,6 +6,7 @@ import { HolyGrail } from "../layouts/HolyGrail";
 import * as WsDFUXref from "src/WsDFUXref";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
@@ -73,6 +74,32 @@ export const XrefFoundFiles: React.FunctionComponent<XrefFoundFilesProps> = ({
             ;
     }, [name, refreshTable, store]);
 
+    const [AttachConfirm, setShowAttachConfirm] = useConfirm({
+        title: nlsHPCC.Attach,
+        message: nlsHPCC.AddTheseFilesToDali + "\n" + selection.map(file => file.name).join("\n"),
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Attach, name, "Found")
+                .then(response => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [name, refreshData, selection])
+    });
+
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedFiles + "\n" + selection.map(file => file.name),
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Delete, name, "Found")
+                .then(response => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [name, refreshData, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -82,34 +109,14 @@ export const XrefFoundFiles: React.FunctionComponent<XrefFoundFilesProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "attach", text: nlsHPCC.Attach, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(file => file.name);
-                if (confirm(nlsHPCC.AddTheseFilesToDali + "\n" + list)) {
-                    WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Attach, name, "Found")
-                        .then(response => {
-                            refreshData();
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowAttachConfirm(true)
         },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(file => file.name);
-                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
-                    WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Delete, name, "Found")
-                        .then(response => {
-                            refreshData();
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ], [name, refreshData, selection, uiState]);
+    ], [refreshData, setShowAttachConfirm, setShowDeleteConfirm, uiState]);
 
     React.useEffect(() => {
         refreshData();
@@ -117,7 +124,13 @@ export const XrefFoundFiles: React.FunctionComponent<XrefFoundFilesProps> = ({
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={<Grid />}
+        main={
+            <>
+                <Grid />
+                <AttachConfirm />
+                <DeleteConfirm />
+            </>
+        }
     />;
 
 };

+ 22 - 13
esp/src/src-react/components/XrefLostFiles.tsx

@@ -6,6 +6,7 @@ import { HolyGrail } from "../layouts/HolyGrail";
 import * as WsDFUXref from "src/WsDFUXref";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
@@ -79,6 +80,19 @@ export const XrefLostFiles: React.FunctionComponent<XrefLostFilesProps> = ({
             ;
     }, [store, name, refreshTable]);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedFiles + "\n" + selection.map(file => file.name),
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefArrayAction(selection, "DeleteLogical", name, "Lost")
+                .then(response => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [name, refreshData, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -88,20 +102,10 @@ export const XrefLostFiles: React.FunctionComponent<XrefLostFilesProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(file => file.name);
-                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
-                    WsDFUXref.DFUXRefArrayAction(selection, "DeleteLogical", name, "Lost")
-                        .then(response => {
-                            refreshData();
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ], [name, refreshData, selection, uiState]);
+    ], [refreshData, setShowDeleteConfirm, uiState]);
 
     React.useEffect(() => {
         refreshData();
@@ -109,7 +113,12 @@ export const XrefLostFiles: React.FunctionComponent<XrefLostFilesProps> = ({
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={<Grid />}
+        main={
+            <>
+                <Grid />
+                <DeleteConfirm />
+            </>
+        }
     />;
 
 };

+ 22 - 13
esp/src/src-react/components/XrefOrphanFiles.tsx

@@ -6,6 +6,7 @@ import { HolyGrail } from "../layouts/HolyGrail";
 import * as WsDFUXref from "src/WsDFUXref";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
@@ -75,6 +76,19 @@ export const XrefOrphanFiles: React.FunctionComponent<XrefOrphanFilesProps> = ({
             ;
     }, [name, refreshTable, store]);
 
+    const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({
+        title: nlsHPCC.Delete,
+        message: nlsHPCC.DeleteSelectedFiles + "\n" + selection.map(file => file.name).join("\n"),
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Delete, name, "Orphan")
+                .then(response => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [name, refreshData, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -84,20 +98,10 @@ export const XrefOrphanFiles: React.FunctionComponent<XrefOrphanFilesProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection,
-            onClick: () => {
-                const list = selection.map(file => file.name);
-                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
-                    WsDFUXref.DFUXRefArrayAction(selection, nlsHPCC.Delete, name, "Orphan")
-                        .then(response => {
-                            refreshData();
-                        })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowDeleteConfirm(true)
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ], [name, refreshData, selection, uiState]);
+    ], [refreshData, setShowDeleteConfirm, uiState]);
 
     React.useEffect(() => {
         refreshData();
@@ -105,7 +109,12 @@ export const XrefOrphanFiles: React.FunctionComponent<XrefOrphanFilesProps> = ({
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={<Grid />}
+        main={
+            <>
+                <Grid />
+                <DeleteConfirm />
+            </>
+        }
     />;
 
 };

+ 46 - 32
esp/src/src-react/components/Xrefs.tsx

@@ -6,6 +6,7 @@ import { HolyGrail } from "../layouts/HolyGrail";
 import * as WsDFUXref from "src/WsDFUXref";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
+import { useConfirm } from "../hooks/confirm";
 import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
@@ -80,6 +81,41 @@ export const Xrefs: React.FunctionComponent<XrefsProps> = ({
             ;
     }, [refreshTable, store]);
 
+    const [CancelConfirm, setShowCancelConfirm] = useConfirm({
+        title: nlsHPCC.CancelAll,
+        message: nlsHPCC.CancelAllMessage,
+        onSubmit: React.useCallback(() => {
+            WsDFUXref.DFUXRefBuildCancel({
+                request: {}
+            })
+                .catch(logger.error)
+                ;
+        }, [])
+    });
+
+    const [GenerateConfirm, setShowGenerateConfirm] = useConfirm({
+        title: nlsHPCC.Generate,
+        message: nlsHPCC.RunningServerStrain,
+        onSubmit: React.useCallback(() => {
+            const requests = [];
+            for (let i = selection.length - 1; i >= 0; --i) {
+                requests.push(
+                    WsDFUXref.DFUXRefBuild({
+                        request: {
+                            Cluster: selection[i].name
+                        }
+                    })
+                );
+            }
+            Promise.all(requests)
+                .then(() => {
+                    refreshData();
+                })
+                .catch(logger.error)
+                ;
+        }, [refreshData, selection])
+    });
+
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -102,42 +138,14 @@ export const Xrefs: React.FunctionComponent<XrefsProps> = ({
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "cancelAll", text: nlsHPCC.CancelAll,
-            onClick: () => {
-                if (confirm(nlsHPCC.CancelAllMessage)) {
-                    WsDFUXref.DFUXRefBuildCancel({
-                        request: {}
-                    })
-                        .catch(logger.error)
-                        ;
-                }
-            }
+            onClick: () => setShowCancelConfirm(true)
         },
         { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "generate", text: nlsHPCC.Generate,
-            onClick: () => {
-                if (confirm(nlsHPCC.RunningServerStrain)) {
-                    const requests = [];
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        requests.push(
-                            WsDFUXref.DFUXRefBuild({
-                                request: {
-                                    Cluster: selection[i].name
-                                }
-                            })
-                        );
-
-                        Promise.all(requests)
-                            .then(() => {
-                                refreshData();
-                            })
-                            .catch(logger.error)
-                            ;
-                    }
-                }
-            },
+            onClick: () => setShowGenerateConfirm(true)
         }
-    ], [refreshData, selection, uiState]);
+    ], [refreshData, selection, setShowCancelConfirm, setShowGenerateConfirm, uiState]);
 
     React.useEffect(() => {
         refreshData();
@@ -145,7 +153,13 @@ export const Xrefs: React.FunctionComponent<XrefsProps> = ({
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={<Grid />}
+        main={
+            <>
+                <Grid />
+                <CancelConfirm />
+                <GenerateConfirm />
+            </>
+        }
     />;
 
 };

+ 0 - 47
esp/src/src-react/components/controls/Confirm.tsx

@@ -1,47 +0,0 @@
-import * as React from "react";
-import { DefaultButton, Dialog, DialogFooter, PrimaryButton } from "@fluentui/react";
-import nlsHPCC from "src/nlsHPCC";
-
-interface ConfirmProps {
-    show: boolean;
-    setShow: (_: boolean) => void;
-    title: string;
-    message: string;
-    onSubmit: () => void;
-}
-
-export const Confirm: React.FunctionComponent<ConfirmProps> = ({
-    show,
-    setShow,
-    title,
-    message,
-    onSubmit = null
-}) => {
-
-    return <Dialog
-        hidden={!show}
-        onDismiss={() => setShow(false)}
-        dialogContentProps={{
-            title: title
-        }}
-        maxWidth={500}
-    >
-        <div>
-            {message.split("\n").map(str => {
-                return <span>{str} <br /></span>;
-            })}
-        </div>
-        <DialogFooter>
-            <PrimaryButton text={nlsHPCC.OK}
-                onClick={() => {
-                    if (typeof onSubmit === "function") {
-                        onSubmit();
-                    }
-                    setShow(false);
-                }}
-            />
-            <DefaultButton text={nlsHPCC.Cancel} onClick={() => setShow(false)} />
-        </DialogFooter>
-    </Dialog>;
-
-};

+ 45 - 0
esp/src/src-react/hooks/confirm.tsx

@@ -0,0 +1,45 @@
+import * as React from "react";
+import { DefaultButton, Dialog, DialogFooter, PrimaryButton } from "@fluentui/react";
+import nlsHPCC from "src/nlsHPCC";
+
+interface useConfirmProps {
+    title: string;
+    message: string;
+    onSubmit: () => void;
+}
+
+export function useConfirm({ title, message, onSubmit }: useConfirmProps): [React.FunctionComponent, (_: boolean) => void] {
+
+    const [show, setShow] = React.useState(false);
+
+    const Confirm = React.useMemo(() => () => {
+        return <Dialog
+            hidden={!show}
+            onDismiss={() => setShow(false)}
+            dialogContentProps={{
+                title: title
+            }}
+            maxWidth={500}
+        >
+            <div>
+                {message.split("\n").map(str => {
+                    return <span>{str} <br /></span>;
+                })}
+            </div>
+            <DialogFooter>
+                <PrimaryButton text={nlsHPCC.OK}
+                    onClick={() => {
+                        if (typeof onSubmit === "function") {
+                            onSubmit();
+                        }
+                        setShow(false);
+                    }}
+                />
+                <DefaultButton text={nlsHPCC.Cancel} onClick={() => setShow(false)} />
+            </DialogFooter>
+        </Dialog>;
+    }, [show, setShow, title, message, onSubmit]);
+
+    return [Confirm, setShow];
+
+}