Преглед на файлове

HPCC-26194 Refactor code to enable exhaustive-deps

Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
Gordon Smith преди 3 години
родител
ревизия
6cdb6f6fd5

+ 112 - 114
esp/src/src-react/components/Activities.tsx

@@ -51,8 +51,113 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
+    //  Grid ---
+    const activity = useConst(ESPActivity.Get());
+    const gridParams = useConst({
+        store: activity.getStore({}),
+        query: {},
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox",
+                sortable: false
+            }),
+            Priority: {
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("priority.png", nlsHPCC.Priority);
+                },
+                width: 25,
+                sortable: false,
+                formatter: function (Priority) {
+                    switch (Priority) {
+                        case "high":
+                            return Utility.getImageHTML("priority_high.png");
+                        case "low":
+                            return Utility.getImageHTML("priority_low.png");
+                    }
+                    return "";
+                }
+            },
+            DisplayName: tree({
+                label: nlsHPCC.TargetWuid,
+                width: 300,
+                sortable: true,
+                shouldExpand: function (row, level, previouslyExpanded) {
+                    if (level === 0) {
+                        return previouslyExpanded === undefined ? true : previouslyExpanded;
+                    }
+                    return previouslyExpanded;
+                },
+                formatter: function (_name, row) {
+                    const img = row.getStateImage();
+                    if (activity.isInstanceOfQueue(row)) {
+                        if (row.ClusterType === 3) {
+                            return `<img src='${img}'/>&nbsp;<a href='#/clusters/${row.ClusterName}' class='dgrid-row-url'>${_name}</a>`;
+                        } else {
+                            return `<img src='${img}'/>&nbsp;${_name}`;
+                        }
+                    }
+                    return `<img src='${img}'/>&nbsp;<a href='#/workunits/${row.Wuid}' class='dgrid-row-url'>${row.Wuid}</a>`;
+                }
+            }),
+            GID: {
+                label: nlsHPCC.Graph, width: 90, sortable: true,
+                formatter: function (_gid, row) {
+                    if (activity.isInstanceOfWorkunit(row)) {
+                        if (row.GraphName) {
+                            return `<a href='#/graphs/${row.GraphName}/${row.GID}' class='dgrid-row-url2'>${row.GraphName}-${row.GID}</a>`;
+                        }
+                    }
+                    return "";
+                }
+            },
+            State: {
+                label: nlsHPCC.State,
+                sortable: false,
+                formatter: function (state, row) {
+                    if (activity.isInstanceOfQueue(row)) {
+                        return row.isNormal() ? "" : row.StatusDetails;
+                    }
+                    if (row.Duration) {
+                        return state + " (" + row.Duration + ")";
+                    } else if (row.Instance && !(state.indexOf && state.indexOf(row.Instance) !== -1)) {
+                        return state + " [" + row.Instance + "]";
+                    }
+                    return state;
+                }
+            },
+            Owner: { label: nlsHPCC.Owner, width: 90, sortable: false },
+            Jobname: { label: nlsHPCC.JobName, sortable: false }
+        },
+        getSelected: function () {
+            const retVal = [];
+            for (const id in this.selection) {
+                const item = activity.resolve(id);
+                if (item) {
+                    retVal.push(item);
+                }
+            }
+            return retVal;
+        }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", {});
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid]);
+
+    React.useEffect(() => {
+        refreshTable();
+        const handle = activity.watch("__hpcc_changedCount", function (item, oldValue, newValue) {
+            refreshTable();
+        });
+        return () => handle.unwatch();
+    }, [activity, grid, refreshTable]);
+
     //  Command Bar  ---
-    const wuPriority = (priority) => {
+    const wuPriority = React.useCallback((priority) => {
         const promises = new DelayedRefresh(refreshTable);
         selection.forEach((item, idx) => {
             const queue = item.get("ESPQueue");
@@ -61,9 +166,9 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
             }
         });
         promises.refresh();
-    };
+    }, [refreshTable, selection]);
 
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => activity.refresh()
@@ -243,117 +348,11 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
                 promises.refresh();
             }
         },
-    ];
+    ], [activity, refreshTable, selection, uiState.clusterNotPausedSelected, uiState.clusterPausedSelected, uiState.thorClusterSelected, uiState.wuCanDown, uiState.wuCanHigh, uiState.wuCanLow, uiState.wuCanNormal, uiState.wuCanUp, uiState.wuSelected, wuPriority]);
 
-    const rightButtons: ICommandBarItemProps[] = [
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
         ...createCopyDownloadSelection(grid, selection, "activities.csv")
-    ];
-
-    //  Grid ---
-    const activity = useConst(ESPActivity.Get());
-    const gridParams = useConst({
-        store: activity.getStore({}),
-        query: {},
-        columns: {
-            col1: selector({
-                width: 27,
-                selectorType: "checkbox",
-                sortable: false
-            }),
-            Priority: {
-                renderHeaderCell: function (node) {
-                    node.innerHTML = Utility.getImageHTML("priority.png", nlsHPCC.Priority);
-                },
-                width: 25,
-                sortable: false,
-                formatter: function (Priority) {
-                    switch (Priority) {
-                        case "high":
-                            return Utility.getImageHTML("priority_high.png");
-                        case "low":
-                            return Utility.getImageHTML("priority_low.png");
-                    }
-                    return "";
-                }
-            },
-            DisplayName: tree({
-                label: nlsHPCC.TargetWuid,
-                width: 300,
-                sortable: true,
-                shouldExpand: function (row, level, previouslyExpanded) {
-                    if (level === 0) {
-                        return previouslyExpanded === undefined ? true : previouslyExpanded;
-                    }
-                    return previouslyExpanded;
-                },
-                formatter: function (_name, row) {
-                    const img = row.getStateImage();
-                    if (activity.isInstanceOfQueue(row)) {
-                        if (row.ClusterType === 3) {
-                            return `<img src='${img}'/>&nbsp;<a href='#/clusters/${row.ClusterName}' class='dgrid-row-url'>${_name}</a>`;
-                        } else {
-                            return `<img src='${img}'/>&nbsp;${_name}`;
-                        }
-                    }
-                    return `<img src='${img}'/>&nbsp;<a href='#/workunits/${row.Wuid}' class='dgrid-row-url'>${row.Wuid}</a>`;
-                }
-            }),
-            GID: {
-                label: nlsHPCC.Graph, width: 90, sortable: true,
-                formatter: function (_gid, row) {
-                    if (activity.isInstanceOfWorkunit(row)) {
-                        if (row.GraphName) {
-                            return `<a href='#/graphs/${row.GraphName}/${row.GID}' class='dgrid-row-url2'>${row.GraphName}-${row.GID}</a>`;
-                        }
-                    }
-                    return "";
-                }
-            },
-            State: {
-                label: nlsHPCC.State,
-                sortable: false,
-                formatter: function (state, row) {
-                    if (activity.isInstanceOfQueue(row)) {
-                        return row.isNormal() ? "" : row.StatusDetails;
-                    }
-                    if (row.Duration) {
-                        return state + " (" + row.Duration + ")";
-                    } else if (row.Instance && !(state.indexOf && state.indexOf(row.Instance) !== -1)) {
-                        return state + " [" + row.Instance + "]";
-                    }
-                    return state;
-                }
-            },
-            Owner: { label: nlsHPCC.Owner, width: 90, sortable: false },
-            Jobname: { label: nlsHPCC.JobName, sortable: false }
-        },
-        getSelected: function () {
-            const retVal = [];
-            for (const id in this.selection) {
-                const item = activity.resolve(id);
-                if (item) {
-                    retVal.push(item);
-                }
-            }
-            return retVal;
-        }
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
-
-    React.useEffect(() => {
-        refreshTable();
-        const handle = activity.watch("__hpcc_changedCount", function (item, oldValue, newValue) {
-            refreshTable();
-        });
-        return () => handle.unwatch();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [grid]);
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -396,8 +395,7 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
             }
         });
         setUIState(state);
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [selection]);
+    }, [activity, selection]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 64 - 65
esp/src/src-react/components/DFUWorkunits.tsx

@@ -58,67 +58,6 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/dfuworkunits/${selection[0].ID}`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/dfuworkunits/${selection[i].ID}`, "_blank");
-                    }
-                }
-            }
-        },
-        {
-            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));
-                }
-            }
-        },
-        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "setFailed", text: nlsHPCC.SetToFailed, disabled: !uiState.hasNotProtected,
-            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "SetToFailed"); }
-        },
-        { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "protect", text: nlsHPCC.Protect, disabled: !uiState.hasNotProtected,
-            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "Protect"); }
-        },
-        {
-            key: "unprotect", text: nlsHPCC.Unprotect, disabled: !uiState.hasProtected,
-            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "Unprotect"); }
-        },
-        { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
-            onClick: () => {
-                setShowFilter(true);
-            }
-        },
-        {
-            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
-            onClick: () => {
-                setMine(!mine);
-            }
-        },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "dfuworkunits.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(store || ESPDFUWorkunit.CreateWUQueryStore({}));
     const gridQuery = useConst(formatQuery(filter));
@@ -172,12 +111,73 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", formatQuery(filter));
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [filter, grid]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/dfuworkunits/${selection[0].ID}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/dfuworkunits/${selection[i].ID}`, "_blank");
+                    }
+                }
+            }
+        },
+        {
+            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));
+                }
+            }
+        },
+        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "setFailed", text: nlsHPCC.SetToFailed, disabled: !uiState.hasNotProtected,
+            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "SetToFailed"); }
+        },
+        { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "protect", text: nlsHPCC.Protect, disabled: !uiState.hasNotProtected,
+            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "Protect"); }
+        },
+        {
+            key: "unprotect", text: nlsHPCC.Unprotect, disabled: !uiState.hasProtected,
+            onClick: () => { FileSpray.DFUWorkunitsAction(selection, "Unprotect"); }
+        },
+        { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
+            onClick: () => {
+                setShowFilter(true);
+            }
+        },
+        {
+            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
+            onClick: () => {
+                setMine(!mine);
+            }
+        },
+    ], [mine, refreshTable, selection, store, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "dfuworkunits.csv")
+    ], [grid, selection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -187,8 +187,7 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
 
     React.useEffect(() => {
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [filter]);
+    }, [filter, refreshTable]);
 
     //  Selection  ---
     React.useEffect(() => {

+ 15 - 3
esp/src/src-react/components/DojoGrid.tsx

@@ -35,7 +35,7 @@ interface DojoGridProps {
 export const DojoGrid: React.FunctionComponent<DojoGridProps> = ({
     type = "PageSel",
     store,
-    query = {},
+    query,
     sort,
     columns,
     setGrid,
@@ -54,8 +54,20 @@ export const DojoGrid: React.FunctionComponent<DojoGridProps> = ({
         }
     });
 
-    return <DojoComponent Widget={Grid} WidgetParams={{ deselectOnRefresh: true, store, query, sort, columns: { ...columns } }} postCreate={grid => {
+    const params = React.useMemo(() => {
+        return {
+            deselectOnRefresh: true,
+            store,
+            query,
+            sort,
+            columns: { ...columns }
+        };
+    }, [columns, query, sort, store]);
+
+    const gridSelInit = React.useCallback(grid => {
         grid.onSelectionChanged(() => setSelection(grid.getSelected()));
         setGrid(grid);
-    }} />;
+    }, [setGrid, setSelection]);
+
+    return <DojoComponent Widget={Grid} WidgetParams={params} postCreate={gridSelInit} />;
 };

+ 3 - 4
esp/src/src-react/components/FileDetails.tsx

@@ -47,8 +47,7 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({
         setProtected(_protected || isProtected);
         setRestricted(restricted || file?.IsRestricted);
 
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [file?.Description, file?.ProtectList?.DFUFileProtect, file?.IsRestricted]);
+    }, [_protected, description, file?.Description, file?.IsRestricted, isProtected, restricted]);
 
     const canSave = file && (
         description !== file.Description ||
@@ -56,7 +55,7 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({
         restricted !== file?.IsRestricted
     );
 
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => {
@@ -82,7 +81,7 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({
             }
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ];
+    ], [_protected, canSave, description, file, logicalFile, refresh, restricted]);
 
     const protectedImage = _protected ? Utility.getImageURL("locked.png") : Utility.getImageURL("unlocked.png");
     const stateImage = Utility.getImageURL(getStateImageName(file as unknown as IFile));

+ 28 - 29
esp/src/src-react/components/FileDetailsGraph.tsx

@@ -41,31 +41,6 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     // const [helpers] = useWorkunitHelpers(wuid);
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/workunits/${file?.Wuid}/graphs/${selection[0].Name}`;
-                } else {
-                    for (let i = 0; i < selection.length; ++i) {
-                        window.open(`#/workunits/${file?.Wuid}/graphs/${selection[i].Name}`, "_blank");
-                    }
-                }
-            }
-        }
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "graphs.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(new Observable(new Memory("Name")));
     const gridQuery = useConst({});
@@ -82,12 +57,37 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", gridQuery);
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid, gridQuery]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/workunits/${file?.Wuid}/graphs/${selection[0].Name}`;
+                } else {
+                    for (let i = 0; i < selection.length; ++i) {
+                        window.open(`#/workunits/${file?.Wuid}/graphs/${selection[i].Name}`, "_blank");
+                    }
+                }
+            }
+        }
+    ], [file?.Wuid, refreshTable, selection, uiState.hasSelection]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "graphs.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -109,8 +109,7 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
             }));
             refreshTable();
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, file?.Graphs?.ECLGraph]);
+    }, [file?.Graphs?.ECLGraph, gridStore, refreshTable]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 3 - 4
esp/src/src-react/components/FileParts.tsx

@@ -37,12 +37,12 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
         CompressedSize: { label: nlsHPCC.CompressedSize, sortable: true, },
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", gridQuery);
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid, gridQuery]);
 
     React.useEffect(() => {
         if (file?.DFUFilePartsOnClusters) {
@@ -61,8 +61,7 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
                 refreshTable();
             }
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, file?.DFUFilePartsOnClusters]);
+    }, [cluster, file?.DFUFilePartsOnClusters, gridStore, refreshTable]);
 
     return <HolyGrail
         main={

+ 51 - 52
esp/src/src-react/components/Files.tsx

@@ -61,54 +61,6 @@ export const Files: React.FunctionComponent<FilesProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/files/${selection[0].NodeGroup}/${selection[0].Name}`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/files/${selection[0].NodeGroup}/${selection[i].Name}`, "_blank");
-                    }
-                }
-            }
-        },
-        {
-            key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection, iconProps: { iconName: "Delete" },
-            onClick: () => {
-                const list = selection.map(s => s.Name);
-                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
-                    WsDfu.DFUArrayAction(selection, "Delete").then(() => refreshTable(true));
-                }
-            }
-        },
-        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
-            onClick: () => {
-                setShowFilter(true);
-            }
-        },
-        {
-            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
-            onClick: () => {
-                setMine(!mine);
-            }
-        },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "logicalfiles.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(store || ESPLogicalFile.CreateLFQueryStore({}));
     const gridQuery = useConst(formatQuery(filter));
@@ -206,12 +158,60 @@ export const Files: React.FunctionComponent<FilesProps> = ({
         Modified: { label: nlsHPCC.ModifiedUTCGMT, width: 162 }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", formatQuery(filter));
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [filter, grid]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/files/${selection[0].NodeGroup}/${selection[0].Name}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/files/${selection[0].NodeGroup}/${selection[i].Name}`, "_blank");
+                    }
+                }
+            }
+        },
+        {
+            key: "delete", text: nlsHPCC.Delete, disabled: !uiState.hasSelection, iconProps: { iconName: "Delete" },
+            onClick: () => {
+                const list = selection.map(s => s.Name);
+                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
+                    WsDfu.DFUArrayAction(selection, "Delete").then(() => refreshTable(true));
+                }
+            }
+        },
+        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
+            onClick: () => {
+                setShowFilter(true);
+            }
+        },
+        {
+            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
+            onClick: () => {
+                setMine(!mine);
+            }
+        },
+    ], [mine, refreshTable, selection, store, uiState.hasSelection]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "logicalfiles.csv")
+    ], [grid, selection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -221,8 +221,7 @@ export const Files: React.FunctionComponent<FilesProps> = ({
 
     React.useEffect(() => {
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [filter]);
+    }, [filter, refreshTable]);
 
     //  Selection  ---
     React.useEffect(() => {

+ 46 - 58
esp/src/src-react/components/Helpers.tsx

@@ -9,7 +9,7 @@ import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
 import { HelperRow, useWorkunitHelpers } from "../hooks/Workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { ShortVerticalDivider } from "./Common";
+import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector } from "./DojoGrid";
 
 function canShowContent(type: string) {
@@ -105,8 +105,47 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [helpers] = useWorkunitHelpers(wuid);
 
+    //  Grid ---
+    const gridStore = useConst(new Observable(new Memory("id")));
+    const gridQuery = useConst({});
+    const gridColumns = useConst({
+        sel: selector({
+            width: 27,
+            selectorType: "checkbox"
+        }),
+        Type: {
+            label: nlsHPCC.Type,
+            width: 160,
+            formatter: function (Type, row) {
+                const target = getTarget(row.id, row);
+                if (target) {
+                    return `<a href='#/text?mode=${target.sourceMode}&src=${encodeURIComponent(target.url)}'>${Type + (row?.Orig?.Description ? " (" + row.Orig.Description + ")" : "")}</a>`;
+                }
+                return Type;
+            }
+        },
+        Description: {
+            label: nlsHPCC.Description
+        },
+        FileSize: {
+            label: nlsHPCC.FileSize,
+            width: 90,
+            renderCell: function (object, value, node, options) {
+                domClass.add(node, "justify-right");
+                node.innerText = Utility.valueCleanUp(value);
+            },
+        }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", gridQuery);
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid, gridQuery]);
+
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => refreshTable()
@@ -156,61 +195,11 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
             }
         }
 
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        {
-            key: "copy", text: nlsHPCC.CopyWUIDs, disabled: true, iconOnly: true, iconProps: { iconName: "Copy" },
-            onClick: () => {
-                //  TODO:  HPCC-25473
-            }
-        },
-        {
-            key: "download", text: nlsHPCC.DownloadToCSV, disabled: true, iconOnly: true, iconProps: { iconName: "Download" },
-            onClick: () => {
-                //  TODO:  HPCC-25473
-            }
-        }
-    ];
-
-    //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("id")));
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        sel: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Type: {
-            label: nlsHPCC.Type,
-            width: 160,
-            formatter: function (Type, row) {
-                const target = getTarget(row.id, row);
-                if (target) {
-                    return `<a href='#/text?mode=${target.sourceMode}&src=${encodeURIComponent(target.url)}'>${Type + (row?.Orig?.Description ? " (" + row.Orig.Description + ")" : "")}</a>`;
-                }
-                return Type;
-            }
-        },
-        Description: {
-            label: nlsHPCC.Description
-        },
-        FileSize: {
-            label: nlsHPCC.FileSize,
-            width: 90,
-            renderCell: function (object, value, node, options) {
-                domClass.add(node, "justify-right");
-                node.innerText = Utility.valueCleanUp(value);
-            },
-        }
-    });
+    ], [refreshTable, selection, uiState.canShowContent, uiState.hasSelection]);
 
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "logicalfiles.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -228,8 +217,7 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
     React.useEffect(() => {
         gridStore.setData(helpers);
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, helpers]);
+    }, [gridStore, helpers, refreshTable]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 8 - 9
esp/src/src-react/components/InfoGrid.tsx

@@ -52,16 +52,16 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
     const [selection, setSelection] = React.useState([]);
 
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         { key: "errors", onRender: () => <Checkbox defaultChecked label={`${filterCounts.error || 0} ${nlsHPCC.Errors}`} onChange={(ev, value) => setErrorChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
         { key: "warnings", onRender: () => <Checkbox defaultChecked label={`${filterCounts.warning || 0} ${nlsHPCC.Warnings}`} onChange={(ev, value) => setWarningChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
         { key: "infos", onRender: () => <Checkbox defaultChecked label={`${filterCounts.info || 0} ${nlsHPCC.Infos}`} onChange={(ev, value) => setInfoChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
         { key: "others", onRender: () => <Checkbox defaultChecked label={`${filterCounts.other || 0} ${nlsHPCC.Others}`} onChange={(ev, value) => setOtherChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> }
-    ];
+    ], [filterCounts.error, filterCounts.info, filterCounts.other, filterCounts.warning]);
 
-    const rightButtons: ICommandBarItemProps[] = [
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
         ...createCopyDownloadSelection(grid, selection, "errorwarnings.csv")
-    ];
+    ], [grid, selection]);
 
     //  Grid ---
     const gridStore = useConst(new Observable(new Memory("id")));
@@ -107,12 +107,12 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
         FileName: { label: nlsHPCC.FileName, field: "", width: 360, sortable: false }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", {});
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid]);
 
     React.useEffect(() => {
         const filterCounts = {
@@ -172,13 +172,12 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
         gridStore.setData(filteredExceptions);
         refreshTable();
         setFilterCounts(filterCounts);
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, exceptions, errorChecked, warningChecked, infoChecked, otherChecked]);
+    }, [errorChecked, exceptions, gridStore, infoChecked, otherChecked, refreshTable, warningChecked]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
         main={
-            <DojoGrid type={"SimpleGrid"} store={gridStore} query={{}} sort={{}} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <DojoGrid type={"SimpleGrid"} store={gridStore} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
         }
     />;
 };

+ 67 - 68
esp/src/src-react/components/Queries.tsx

@@ -68,70 +68,6 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/queries/${selection[0].QuerySetId}/${selection[0].Id}`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/queries/${selection[i].QuerySetId}/${selection[i].Id}`, "_blank");
-                    }
-                }
-            }
-        },
-        {
-            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));
-                }
-            }
-        },
-        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "Suspend", text: nlsHPCC.Suspend, disabled: !uiState.isSuspended,
-            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Suspend"); }
-        },
-        {
-            key: "Unsuspend", text: nlsHPCC.Unsuspend, disabled: !uiState.isNotSuspended,
-            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Unsuspend"); }
-        },
-        {
-            key: "Activate", text: nlsHPCC.Activate, disabled: !uiState.isActive,
-            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Activate"); }
-        },
-        {
-            key: "Deactivate", text: nlsHPCC.Deactivate, disabled: !uiState.isNotActive,
-            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Deactivate"); }
-        },
-        { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "filter", text: nlsHPCC.Filter, disabled: store !== undefined || wuid !== undefined, iconProps: { iconName: "Filter" },
-            onClick: () => {
-                setShowFilter(true);
-            }
-        },
-        {
-            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
-            onClick: () => {
-                setMine(!mine);
-            }
-        },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "roxiequeries.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(store || ESPQuery.CreateQueryStore({}));
     const gridQuery = useConst(formatQuery(filter, wuid));
@@ -231,12 +167,76 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", formatQuery(filter, wuid));
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [filter, grid, wuid]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/queries/${selection[0].QuerySetId}/${selection[0].Id}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/queries/${selection[i].QuerySetId}/${selection[i].Id}`, "_blank");
+                    }
+                }
+            }
+        },
+        {
+            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));
+                }
+            }
+        },
+        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "Suspend", text: nlsHPCC.Suspend, disabled: !uiState.isSuspended,
+            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Suspend"); }
+        },
+        {
+            key: "Unsuspend", text: nlsHPCC.Unsuspend, disabled: !uiState.isNotSuspended,
+            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Unsuspend"); }
+        },
+        {
+            key: "Activate", text: nlsHPCC.Activate, disabled: !uiState.isActive,
+            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Activate"); }
+        },
+        {
+            key: "Deactivate", text: nlsHPCC.Deactivate, disabled: !uiState.isNotActive,
+            onClick: () => { WsWorkunits.WUQuerysetQueryAction(selection, "Deactivate"); }
+        },
+        { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "filter", text: nlsHPCC.Filter, disabled: store !== undefined || wuid !== undefined, iconProps: { iconName: "Filter" },
+            onClick: () => {
+                setShowFilter(true);
+            }
+        },
+        {
+            key: "mine", text: nlsHPCC.Mine, disabled: true, iconProps: { iconName: "Contact" }, canCheck: true, checked: mine,
+            onClick: () => {
+                setMine(!mine);
+            }
+        },
+    ], [mine, refreshTable, selection, store, uiState.hasSelection, uiState.isActive, uiState.isNotActive, uiState.isNotSuspended, uiState.isSuspended, wuid]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "roxiequeries.csv")
+    ], [grid, selection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -246,8 +246,7 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
 
     React.useEffect(() => {
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [filter]);
+    }, [filter, refreshTable]);
 
     //  Selection  ---
     React.useEffect(() => {

+ 31 - 43
esp/src/src-react/components/Resources.tsx

@@ -6,7 +6,7 @@ import * as Observable from "dojo/store/Observable";
 import nlsHPCC from "src/nlsHPCC";
 import { useWorkunitResources } from "../hooks/Workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { ShortVerticalDivider } from "./Common";
+import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector } from "./DojoGrid";
 
 const defaultUIState = {
@@ -26,8 +26,32 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [resources] = useWorkunitResources(wuid);
 
+    //  Grid ---
+    const gridStore = useConst(new Observable(new AlphaNumSortMemory("DisplayPath", { Name: true, Value: true })));
+    const gridQuery = useConst({});
+    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
+    const gridColumns = useConst({
+        col1: selector({
+            width: 27,
+            selectorType: "checkbox"
+        }),
+        DisplayPath: {
+            label: nlsHPCC.Name, sortable: true,
+            formatter: function (url, row) {
+                return `<a href='#/iframe?src=${encodeURIComponent(`WsWorkunits/${row.URL}`)}' class='dgrid-row-url'>${url}</a>`;
+            }
+        }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", gridQuery);
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid, gridQuery]);
+
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => refreshTable()
@@ -57,46 +81,11 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
                 }
             }
         },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        {
-            key: "copy", text: nlsHPCC.CopyWUIDs, disabled: true, iconOnly: true, iconProps: { iconName: "Copy" },
-            onClick: () => {
-                //  TODO:  HPCC-25473
-            }
-        },
-        {
-            key: "download", text: nlsHPCC.DownloadToCSV, disabled: true, iconOnly: true, iconProps: { iconName: "Download" },
-            onClick: () => {
-                //  TODO:  HPCC-25473
-            }
-        }
-    ];
-
-    //  Grid ---
-    const gridStore = useConst(new Observable(new AlphaNumSortMemory("DisplayPath", { Name: true, Value: true })));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        DisplayPath: {
-            label: nlsHPCC.Name, sortable: true,
-            formatter: function (url, row) {
-                return `<a href='#/iframe?src=${encodeURIComponent(`WsWorkunits/${row.URL}`)}' class='dgrid-row-url'>${url}</a>`;
-            }
-        }
-    });
+    ], [refreshTable, selection, uiState.hasSelection]);
 
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "roxiequeries.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -117,8 +106,7 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
             };
         }));
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, resources]);
+    }, [gridStore, refreshTable, resources, wuid]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 42 - 43
esp/src/src-react/components/Results.tsx

@@ -26,43 +26,6 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [results] = useWorkunitResults(wuid);
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/workunits/${wuid}/outputs/${selection[0].Name}`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/workunits/${wuid}/outputs/${selection[i].Name}`, "_blank");
-                    }
-                }
-            }
-        },
-        {
-            key: "open legacy", text: nlsHPCC.OpenLegacyMode, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/workunits/${wuid}/outputs/${selection[0].Name}/legacy`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/workunits/${wuid}/outputs/${selection[i].Name}/legacy`, "_blank");
-                    }
-                }
-            }
-        },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "results.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
     const gridQuery = useConst({});
@@ -93,7 +56,7 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
             label: nlsHPCC.Views, sortable: true,
             formatter: function (ResultViews, idx) {
                 let retVal = "";
-                ResultViews.forEach((item, idx) => {
+                ResultViews?.forEach((item, idx) => {
                     retVal += "<a href='#' onClick='return false;' viewName=" + encodeURIComponent(item) + " class='dgrid-row-url3'>" + item + "</a>&nbsp;";
                 });
                 return retVal;
@@ -101,12 +64,49 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", gridQuery);
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid, gridQuery]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/workunits/${wuid}/outputs/${selection[0].Name}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/workunits/${wuid}/outputs/${selection[i].Name}`, "_blank");
+                    }
+                }
+            }
+        },
+        {
+            key: "open legacy", text: nlsHPCC.OpenLegacyMode, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/workunits/${wuid}/outputs/${selection[0].Name}/legacy`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/workunits/${wuid}/outputs/${selection[i].Name}/legacy`, "_blank");
+                    }
+                }
+            }
+        },
+    ], [refreshTable, selection, uiState.hasSelection, wuid]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "results.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -121,7 +121,7 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
 
     React.useEffect(() => {
         gridStore.setData(results.map(row => {
-            const tmp: any = row?.ResultViews;
+            const tmp: any = row.ResultViews;
             return {
                 __hpcc_id: row.Name,
                 Name: row.Name,
@@ -132,8 +132,7 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
             };
         }));
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, results]);
+    }, [gridStore, refreshTable, results]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 31 - 31
esp/src/src-react/components/Search.tsx

@@ -33,6 +33,32 @@ export const Search: React.FunctionComponent<SearchProps> = ({
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [searchCount, setSearchCount] = React.useState(0);
 
+    //  Grid ---
+    const gridColumns = useConst({
+        col1: selector({ width: 27, selectorType: "checkbox" }),
+        Type: {
+            label: nlsHPCC.What, width: 108, sortable: true,
+            formatter: function (type, idx) {
+                return "<a href='#' onClick='return false;' rowIndex=" + idx + " class='" + "SearchTypeClick'>" + type + "</a>";
+            }
+        },
+        Reason: { label: nlsHPCC.Where, width: 108, sortable: true },
+        Summary: {
+            label: nlsHPCC.Who, sortable: true,
+            formatter: function (summary) {
+                return "<a href='#' onClick='return false;' class='dgrid-row-url'>" + summary + "</a>";
+            }
+        }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", {});
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid]);
+
+    //  Search
     const search = useConst(new ESPSearch(
         searchCount => {
             setSearchCount(searchCount);
@@ -51,11 +77,10 @@ export const Search: React.FunctionComponent<SearchProps> = ({
             search.searchAll(searchText);
             refreshTable();
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [searchText]);
+    }, [progress, refreshTable, search, searchText]);
 
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => refreshTable()
@@ -80,36 +105,11 @@ export const Search: React.FunctionComponent<SearchProps> = ({
                 setMine(!mine);
             }
         },
-    ];
+    ], [mine, refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons: ICommandBarItemProps[] = [
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
         ...createCopyDownloadSelection(grid, selection, "search.csv")
-    ];
-
-    //  Grid ---
-    const gridColumns = useConst({
-        col1: selector({ width: 27, selectorType: "checkbox" }),
-        Type: {
-            label: nlsHPCC.What, width: 108, sortable: true,
-            formatter: function (type, idx) {
-                return "<a href='#' onClick='return false;' rowIndex=" + idx + " class='" + "SearchTypeClick'>" + type + "</a>";
-            }
-        },
-        Reason: { label: nlsHPCC.Where, width: 108, sortable: true },
-        Summary: {
-            label: nlsHPCC.Who, sortable: true,
-            formatter: function (summary) {
-                return "<a href='#' onClick='return false;' class='dgrid-row-url'>" + summary + "</a>";
-            }
-        }
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {

+ 28 - 29
esp/src/src-react/components/SourceFiles.tsx

@@ -39,31 +39,6 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [variables] = useWorkunitSourceFiles(wuid);
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-        {
-            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
-            onClick: () => {
-                if (selection.length === 1) {
-                    window.location.href = `#/files/${selection[0].Name}`;
-                } else {
-                    for (let i = selection.length - 1; i >= 0; --i) {
-                        window.open(`#/files/${selection[i].Name}`, "_blank");
-                    }
-                }
-            }
-        },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "sourcefiles.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(new Observable(new TreeStore("Name", { Name: true, Value: true })));
     const gridSort = useConst([{ attribute: "Name", "descending": false }]);
@@ -89,12 +64,37 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", gridQuery);
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid, gridQuery]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/files/${selection[0].Name}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/files/${selection[i].Name}`, "_blank");
+                    }
+                }
+            }
+        },
+    ], [refreshTable, selection, uiState.hasSelection]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "sourcefiles.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -110,8 +110,7 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     React.useEffect(() => {
         gridStore.setData(variables);
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, variables]);
+    }, [gridStore, refreshTable, variables]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 27 - 24
esp/src/src-react/components/SuperFiles.tsx

@@ -6,7 +6,7 @@ import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
 import { useFile } from "../hooks/File";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { ShortVerticalDivider } from "./Common";
+import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector } from "./DojoGrid";
 
 const defaultUIState = {
@@ -28,8 +28,27 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
+    //  Grid ---
+    const gridStore = useConst(new Observable(new Memory("Name")));
+    const gridSort = useConst([{ attribute: "Name", "descending": false }]);
+    const gridQuery = useConst({});
+    const gridColumns = useConst({
+        col1: selector({
+            width: 27,
+            selectorType: "checkbox"
+        }),
+        Name: { label: nlsHPCC.Name, sortable: true, },
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", gridQuery);
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid, gridQuery]);
+
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => refreshTable()
@@ -47,26 +66,11 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
                 }
             }
         },
-    ];
+    ], [cluster, refreshTable, selection, uiState.hasSelection]);
 
-    //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Name")));
-    const gridSort = useConst([{ attribute: "Name", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Name: { label: nlsHPCC.Name, sortable: true, },
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "syper_files.csv")
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {
@@ -84,11 +88,10 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
             gridStore.setData(file?.Superfiles?.DFULogicalFile);
             refreshTable();
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, file?.Superfiles?.DFULogicalFile]);
+    }, [file, gridStore, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={[]} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
         main={
             <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
         }

+ 17 - 18
esp/src/src-react/components/Variables.tsx

@@ -21,19 +21,6 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [variables] = useWorkunitVariables(wuid);
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => refreshTable()
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "variables.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
     const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
@@ -43,12 +30,12 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
         Value: { label: nlsHPCC.Value }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", {});
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid]);
 
     React.useEffect(() => {
         gridStore.setData(variables.map((row, idx) => {
@@ -58,13 +45,25 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
             };
         }));
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, variables]);
+    }, [gridStore, refreshTable, variables]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+    ], [refreshTable]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "variables.csv")
+    ], [grid, selection]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
         main={
-            <DojoGrid store={gridStore} query={{}} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <DojoGrid store={gridStore} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
         }
     />;
 };

+ 18 - 19
esp/src/src-react/components/Workflows.tsx

@@ -21,21 +21,6 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [workflows, , refreshWorkflow] = useWorkunitWorkflows(wuid);
 
-    //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
-        {
-            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
-            onClick: () => {
-                refreshWorkflow();
-            }
-        },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ];
-
-    const rightButtons: ICommandBarItemProps[] = [
-        ...createCopyDownloadSelection(grid, selection, "workflows.csv")
-    ];
-
     //  Grid ---
     const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
     const gridQuery = useConst({});
@@ -63,12 +48,12 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
         }
     });
 
-    const refreshTable = (clearSelection = false) => {
+    const refreshTable = React.useCallback((clearSelection = false) => {
         grid?.set("query", gridQuery);
         if (clearSelection) {
             grid?.clearSelection();
         }
-    };
+    }, [grid, gridQuery]);
 
     React.useEffect(() => {
         gridStore.setData(workflows.map(row => {
@@ -78,8 +63,22 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
             };
         }));
         refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, workflows]);
+    }, [gridStore, refreshTable, workflows]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => {
+                refreshWorkflow();
+            }
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+    ], [refreshWorkflow]);
+
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, "workflows.csv")
+    ], [grid, selection]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}

+ 5 - 6
esp/src/src-react/components/WorkunitDetails.tsx

@@ -76,8 +76,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
         setDescription(description || workunit?.Description);
         setProtected(_protected || workunit?.Protected);
 
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [workunit?.Jobname, workunit?.Description, workunit?.Protected]);
+    }, [_protected, description, jobname, workunit?.Description, workunit?.Jobname, workunit?.Protected]);
 
     const canSave = workunit && (
         jobname !== workunit.Jobname ||
@@ -85,7 +84,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
         _protected !== workunit.Protected
     );
 
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => {
@@ -110,9 +109,9 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
             }
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
-    ];
+    ], [_protected, canSave, description, jobname, workunit, wuid]);
 
-    const rightButtons: ICommandBarItemProps[] = [
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "star", iconProps: { iconName: isFavorite ? "FavoriteStarFill" : "FavoriteStar" },
             onClick: () => {
@@ -123,7 +122,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
                 }
             }
         }
-    ];
+    ], [addFavorite, isFavorite, removeFavorite]);
 
     const serviceNames = workunit?.ServiceNames?.Item?.join("\n") || "";
     const resourceCount = workunit?.ResourceURLCount > 1 ? workunit?.ResourceURLCount - 1 : undefined;

+ 64 - 65
esp/src/src-react/components/Workunits.tsx

@@ -76,8 +76,68 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
     const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
+    //  Grid ---
+    const gridStore = useConst(() => store ? store : ESPWorkunit.CreateWUQueryStore({}));
+    const gridQuery = useConst(formatQuery(filter));
+    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
+    const gridColumns = useConst({
+        col1: selector({
+            width: 27,
+            selectorType: "checkbox"
+        }),
+        Protected: {
+            renderHeaderCell: function (node) {
+                node.innerHTML = Utility.getImageHTML("locked.png", nlsHPCC.Protected);
+            },
+            width: 25,
+            sortable: false,
+            formatter: function (_protected) {
+                if (_protected === true) {
+                    return Utility.getImageHTML("locked.png");
+                }
+                return "";
+            }
+        },
+        Wuid: {
+            label: nlsHPCC.WUID, width: 180,
+            formatter: function (Wuid) {
+                const wu = ESPWorkunit.Get(Wuid);
+                return `${wu.getStateImageHTML()}&nbsp;<a href='#/workunits/${Wuid}' class='dgrid-row-url''>${Wuid}</a>`;
+            }
+        },
+        Owner: { label: nlsHPCC.Owner, width: 90 },
+        Jobname: { label: nlsHPCC.JobName, width: 500 },
+        Cluster: { label: nlsHPCC.Cluster, width: 90 },
+        RoxieCluster: { label: nlsHPCC.RoxieCluster, width: 99 },
+        State: { label: nlsHPCC.State, width: 90 },
+        TotalClusterTime: {
+            label: nlsHPCC.TotalClusterTime, width: 117,
+            renderCell: function (object, value, node) {
+                domClass.add(node, "justify-right");
+                node.innerText = value;
+            }
+        }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        grid?.set("query", formatQuery(filter));
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [filter, grid]);
+
+    //  Filter  ---
+    const filterFields: Fields = {};
+    for (const fieldID in FilterFields) {
+        filterFields[fieldID] = { ...FilterFields[fieldID], value: filter[fieldID] };
+    }
+
+    React.useEffect(() => {
+        refreshTable();
+    }, [refreshTable, filter, store?.data]);
+
     //  Command Bar  ---
-    const buttons: ICommandBarItemProps[] = [
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
             key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
             onClick: () => refreshTable()
@@ -135,72 +195,11 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
                 setMine(!mine);
             }
         },
-    ];
+    ], [mine, refreshTable, selection, store, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
 
-    const rightButtons: ICommandBarItemProps[] = [
+    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
         ...createCopyDownloadSelection(grid, selection, "workunits.csv")
-    ];
-
-    //  Grid ---
-    const gridStore = useConst(store || ESPWorkunit.CreateWUQueryStore({}));
-    const gridQuery = useConst(formatQuery(filter));
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Protected: {
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("locked.png", nlsHPCC.Protected);
-            },
-            width: 25,
-            sortable: false,
-            formatter: function (_protected) {
-                if (_protected === true) {
-                    return Utility.getImageHTML("locked.png");
-                }
-                return "";
-            }
-        },
-        Wuid: {
-            label: nlsHPCC.WUID, width: 180,
-            formatter: function (Wuid) {
-                const wu = ESPWorkunit.Get(Wuid);
-                return `${wu.getStateImageHTML()}&nbsp;<a href='#/workunits/${Wuid}' class='dgrid-row-url''>${Wuid}</a>`;
-            }
-        },
-        Owner: { label: nlsHPCC.Owner, width: 90 },
-        Jobname: { label: nlsHPCC.JobName, width: 500 },
-        Cluster: { label: nlsHPCC.Cluster, width: 90 },
-        RoxieCluster: { label: nlsHPCC.RoxieCluster, width: 99 },
-        State: { label: nlsHPCC.State, width: 90 },
-        TotalClusterTime: {
-            label: nlsHPCC.TotalClusterTime, width: 117,
-            renderCell: function (object, value, node) {
-                domClass.add(node, "justify-right");
-                node.innerText = value;
-            }
-        }
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", formatQuery(filter));
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    };
-
-    //  Filter  ---
-    const filterFields: Fields = {};
-    for (const fieldID in FilterFields) {
-        filterFields[fieldID] = { ...FilterFields[fieldID], value: filter[fieldID] };
-    }
-
-    React.useEffect(() => {
-        refreshTable();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [filter, store?.data]);
+    ], [grid, selection]);
 
     //  Selection  ---
     React.useEffect(() => {

+ 10 - 10
esp/src/src-react/components/WorkunitsDashboard.tsx

@@ -63,11 +63,11 @@ export const WorkunitsDashboard: React.FunctionComponent<WorkunitsDashboardProps
     }, [filterProps.lastNDays]);
 
     //  Cluster Chart ---
-    const clusterChart = React.useRef(
+    const clusterChart = useConst(
         new Bar()
             .columns(["Cluster", "Count"])
             .on("click", (row, col, sel) => pushParamExact("cluster", sel ? row.Cluster : undefined))
-    ).current;
+    );
 
     const clusterPipeline = chain(
         filter<WorkunitEx>(row => filterProps.state === undefined || row.State === filterProps.state),
@@ -84,11 +84,11 @@ export const WorkunitsDashboard: React.FunctionComponent<WorkunitsDashboardProps
         ;
 
     //  Owner Chart ---
-    const ownerChart = React.useRef(
+    const ownerChart = useConst(
         new Column()
             .columns(["Owner", "Count"])
             .on("click", (row, col, sel) => pushParamExact("owner", sel ? row.Owner : undefined))
-    ).current;
+    );
 
     const ownerPipeline = chain(
         filter<WorkunitEx>(row => filterProps.cluster === undefined || row.Cluster === filterProps.cluster),
@@ -105,11 +105,11 @@ export const WorkunitsDashboard: React.FunctionComponent<WorkunitsDashboardProps
         ;
 
     //  State Chart ---
-    const stateChart = React.useRef(
+    const stateChart = useConst(
         new Pie()
             .columns(["State", "Count"])
             .on("click", (row, col, sel) => pushParamExact("state", sel ? row.State : undefined))
-    ).current;
+    );
 
     const statePipeline = chain(
         filter<WorkunitEx>(row => filterProps.cluster === undefined || row.Cluster === filterProps.cluster),
@@ -125,11 +125,11 @@ export const WorkunitsDashboard: React.FunctionComponent<WorkunitsDashboardProps
         ;
 
     //  Protected Chart ---
-    const protectedChart = React.useRef(
+    const protectedChart = useConst(
         new Pie()
             .columns(["Protected", "Count"])
             .on("click", (row, col, sel) => pushParamExact("protected", sel ? row.Protected === "true" : undefined))
-    ).current;
+    );
 
     const protectedPipeline = chain(
         filter<WorkunitEx>(row => filterProps.cluster === undefined || row.Cluster === filterProps.cluster),
@@ -144,14 +144,14 @@ export const WorkunitsDashboard: React.FunctionComponent<WorkunitsDashboardProps
         ;
 
     //  Day Chart ---
-    const dayChart = React.useRef(
+    const dayChart = useConst(
         new Area()
             .columns(["Day", "Count"])
             .xAxisType("time")
             .interpolate("cardinal")
             // .xAxisTypeTimePattern("")
             .on("click", (row, col, sel) => pushParamExact("day", sel ? row.Day : undefined))
-    ).current;
+    );
 
     const dayPipeline = chain(
         filter<WorkunitEx>(row => filterProps.cluster === undefined || row.Cluster === filterProps.cluster),

+ 0 - 1
esp/src/src-react/components/forms/Fields.tsx

@@ -176,7 +176,6 @@ export const TargetClusterTextField: React.FunctionComponent<TargetClusterTextFi
                 })
             );
         });
-        // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);
 
     return <Dropdown key={key} label={label} selectedKey={selectedKey} optional className={className} onChange={onChange} placeholder={placeholder} options={targetClusters} />;

+ 2 - 4
esp/src/src-react/components/forms/Forms.tsx

@@ -39,8 +39,7 @@ export const TableForm: React.FunctionComponent<TableFormProps> = ({
     React.useEffect(() => {
         if (doSubmit === false) return;
         onSubmit(fieldsToRequest(localFields));
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [doSubmit]);
+    }, [doSubmit, localFields, onSubmit]);
 
     React.useEffect(() => {
         if (doReset === false) return;
@@ -55,8 +54,7 @@ export const TableForm: React.FunctionComponent<TableFormProps> = ({
         }
         setLocalFields(localFields);
         onReset(fieldsToRequest(localFields));
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [doReset]);
+    }, [doReset, localFields, onReset]);
 
     return <TableGroup fields={localFields} onChange={(id, value) => {
         const field = localFields[id];

+ 2 - 4
esp/src/src-react/layouts/DojoAdapter.tsx

@@ -88,8 +88,7 @@ export const DojoAdapter: React.FunctionComponent<DojoAdapterProps> = ({
             }
             widget = null;  //  Avoid race condition  ---
         };
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []);
+    }, [delayProps, onWidgetMount, params, uid, widgetClass, widgetClassID]);
 
     widget?.resize();
     return <div ref={myRef} style={{ width: "100%", height: "100%" }}>{nlsHPCC.Loading} {widgetClassID}...</div>;
@@ -129,8 +128,7 @@ export const DojoComponent: React.FunctionComponent<DojoComponentProps> = ({
         return () => {
             w.destroyRecursive();
         };
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []);
+    }, [Widget, WidgetParams, divID, id, postCreate]);
 
     return <div style={{ width: "100%", height: "100%", position: "relative" }}>
         <div id={divID} className="dojo-component">