Selaa lähdekoodia

HPCC-26555 ECL Watch common up Grid code

Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
Gordon Smith 3 vuotta sitten
vanhempi
commit
bebbb0c76d
35 muutettua tiedostoa jossa 1122 lisäystä ja 1307 poistoa
  1. 44 44
      esp/src/package-lock.json
  2. 4 4
      esp/src/package.json
  3. 10 22
      esp/src/src-react/components/Activities.tsx
  4. 53 67
      esp/src/src-react/components/DFUWorkunits.tsx
  5. 14 20
      esp/src/src-react/components/FileBlooms.tsx
  6. 21 31
      esp/src/src-react/components/FileDetailsGraph.tsx
  7. 36 44
      esp/src/src-react/components/FileHistory.tsx
  8. 17 23
      esp/src/src-react/components/FileParts.tsx
  9. 82 96
      esp/src/src-react/components/Files.tsx
  10. 36 45
      esp/src/src-react/components/Helpers.tsx
  11. 48 58
      esp/src/src-react/components/InfoGrid.tsx
  12. 99 114
      esp/src/src-react/components/LandingZone.tsx
  13. 15 25
      esp/src/src-react/components/LogViewer.tsx
  14. 23 28
      esp/src/src-react/components/PackageMapParts.tsx
  15. 31 42
      esp/src/src-react/components/PackageMaps.tsx
  16. 24 32
      esp/src/src-react/components/ProtectedBy.tsx
  17. 101 115
      esp/src/src-react/components/Queries.tsx
  18. 15 20
      esp/src/src-react/components/QueryErrors.tsx
  19. 22 26
      esp/src/src-react/components/QueryGraphs.tsx
  20. 13 18
      esp/src/src-react/components/QueryLibrariesUsed.tsx
  21. 20 25
      esp/src/src-react/components/QueryLogicalFiles.tsx
  22. 25 30
      esp/src/src-react/components/QuerySummaryStats.tsx
  23. 20 25
      esp/src/src-react/components/QuerySuperFiles.tsx
  24. 22 31
      esp/src/src-react/components/Resources.tsx
  25. 43 52
      esp/src/src-react/components/Results.tsx
  26. 30 37
      esp/src/src-react/components/Search.tsx
  27. 31 39
      esp/src/src-react/components/SourceFiles.tsx
  28. 19 28
      esp/src/src-react/components/SuperFiles.tsx
  29. 23 33
      esp/src/src-react/components/Users.tsx
  30. 16 25
      esp/src/src-react/components/Variables.tsx
  31. 29 39
      esp/src/src-react/components/Workflows.tsx
  32. 45 59
      esp/src/src-react/components/Workunits.tsx
  33. 38 0
      esp/src/src-react/hooks/deepHooks.ts
  34. 0 10
      esp/src/src-react/hooks/grid.ts
  35. 53 0
      esp/src/src-react/hooks/grid.tsx

+ 44 - 44
esp/src/package-lock.json

@@ -223,15 +223,15 @@
       }
     },
     "@fluentui/react": {
-      "version": "8.34.0",
-      "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.34.0.tgz",
-      "integrity": "sha512-mJdHS0EbYe1wQA5lgV88yd/8B5Favuw/HBS87YCh/zw3KWthd3W6/1N/mf8stlCNAn0uk7cJaxWXq8rB8vJ2eA==",
+      "version": "8.34.4",
+      "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.34.4.tgz",
+      "integrity": "sha512-P5NHFH2lbiS6r07DXSuDCT39BGrXpALImdaL9c4EiGmQZx20Qpsvud8kVGZfl7H6lg7ukwdkmqrDOBev/LrQJg==",
       "requires": {
         "@fluentui/date-time-utilities": "^8.2.2",
         "@fluentui/font-icons-mdl2": "^8.1.11",
         "@fluentui/foundation-legacy": "^8.1.11",
         "@fluentui/merge-styles": "^8.1.5",
-        "@fluentui/react-focus": "^8.2.2",
+        "@fluentui/react-focus": "^8.2.3",
         "@fluentui/react-hooks": "^8.3.2",
         "@fluentui/react-window-provider": "^2.1.4",
         "@fluentui/set-version": "^8.1.4",
@@ -379,9 +379,9 @@
       }
     },
     "@fluentui/react-focus": {
-      "version": "8.2.2",
-      "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.2.2.tgz",
-      "integrity": "sha512-cmWPphKuFFPqvxyjmhH4r1v5lw8D3HytSgn/LaMQEHhT6RGuLLnx17QDZBUYCrZ0vyBf3nGnO1lsw+EGGsc1SQ==",
+      "version": "8.2.3",
+      "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.2.3.tgz",
+      "integrity": "sha512-TO0KL4tXGaNgI8Hz8pSMK/nNnXdSdNkCM4qGWySpwQA0/1+Td5rGmLEYJbPUIuKSz6eweCZlOCLM1CekSK8ncQ==",
       "requires": {
         "@fluentui/keyboard-key": "^0.3.4",
         "@fluentui/merge-styles": "^8.1.5",
@@ -1481,13 +1481,13 @@
       "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.0.tgz",
-      "integrity": "sha512-iPKZTZNavAlOhfF4gymiSuUkgLne/nh5Oz2/mdiUmuZVD42m9PapnCnzjxuDsnpnbH3wT5s2D8bw6S39TC6GNw==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.2.tgz",
+      "integrity": "sha512-w63SCQ4bIwWN/+3FxzpnWrDjQRXVEGiTt9tJTRptRXeFvdZc/wLiz3FQUwNQ2CVoRGI6KUWMNUj/pk63noUfcA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "4.31.0",
-        "@typescript-eslint/scope-manager": "4.31.0",
+        "@typescript-eslint/experimental-utils": "4.31.2",
+        "@typescript-eslint/scope-manager": "4.31.2",
         "debug": "^4.3.1",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.1.0",
@@ -1496,55 +1496,55 @@
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.0.tgz",
-      "integrity": "sha512-Hld+EQiKLMppgKKkdUsLeVIeEOrwKc2G983NmznY/r5/ZtZCDvIOXnXtwqJIgYz/ymsy7n7RGvMyrzf1WaSQrw==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.2.tgz",
+      "integrity": "sha512-3tm2T4nyA970yQ6R3JZV9l0yilE2FedYg8dcXrTar34zC9r6JB7WyBQbpIVongKPlhEMjhQ01qkwrzWy38Bk1Q==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.7",
-        "@typescript-eslint/scope-manager": "4.31.0",
-        "@typescript-eslint/types": "4.31.0",
-        "@typescript-eslint/typescript-estree": "4.31.0",
+        "@typescript-eslint/scope-manager": "4.31.2",
+        "@typescript-eslint/types": "4.31.2",
+        "@typescript-eslint/typescript-estree": "4.31.2",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.0.tgz",
-      "integrity": "sha512-oWbzvPh5amMuTmKaf1wp0ySxPt2ZXHnFQBN2Szu1O//7LmOvgaKTCIDNLK2NvzpmVd5A2M/1j/rujBqO37hj3w==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.2.tgz",
+      "integrity": "sha512-EcdO0E7M/sv23S/rLvenHkb58l3XhuSZzKf6DBvLgHqOYdL6YFMYVtreGFWirxaU2mS1GYDby3Lyxco7X5+Vjw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "4.31.0",
-        "@typescript-eslint/types": "4.31.0",
-        "@typescript-eslint/typescript-estree": "4.31.0",
+        "@typescript-eslint/scope-manager": "4.31.2",
+        "@typescript-eslint/types": "4.31.2",
+        "@typescript-eslint/typescript-estree": "4.31.2",
         "debug": "^4.3.1"
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.0.tgz",
-      "integrity": "sha512-LJ+xtl34W76JMRLjbaQorhR0hfRAlp3Lscdiz9NeI/8i+q0hdBZ7BsiYieLoYWqy+AnRigaD3hUwPFugSzdocg==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.2.tgz",
+      "integrity": "sha512-2JGwudpFoR/3Czq6mPpE8zBPYdHWFGL6lUNIGolbKQeSNv4EAiHaR5GVDQaLA0FwgcdcMtRk+SBJbFGL7+La5w==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.31.0",
-        "@typescript-eslint/visitor-keys": "4.31.0"
+        "@typescript-eslint/types": "4.31.2",
+        "@typescript-eslint/visitor-keys": "4.31.2"
       }
     },
     "@typescript-eslint/types": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.0.tgz",
-      "integrity": "sha512-9XR5q9mk7DCXgXLS7REIVs+BaAswfdHhx91XqlJklmqWpTALGjygWVIb/UnLh4NWhfwhR5wNe1yTyCInxVhLqQ==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.2.tgz",
+      "integrity": "sha512-kWiTTBCTKEdBGrZKwFvOlGNcAsKGJSBc8xLvSjSppFO88AqGxGNYtF36EuEYG6XZ9vT0xX8RNiHbQUKglbSi1w==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.0.tgz",
-      "integrity": "sha512-QHl2014t3ptg+xpmOSSPn5hm4mY8D4s97ftzyk9BZ8RxYQ3j73XcwuijnJ9cMa6DO4aLXeo8XS3z1omT9LA/Eg==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.2.tgz",
+      "integrity": "sha512-ieBq8U9at6PvaC7/Z6oe8D3czeW5d//Fo1xkF/s9394VR0bg/UaMYPdARiWyKX+lLEjY3w/FNZJxitMsiWv+wA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.31.0",
-        "@typescript-eslint/visitor-keys": "4.31.0",
+        "@typescript-eslint/types": "4.31.2",
+        "@typescript-eslint/visitor-keys": "4.31.2",
         "debug": "^4.3.1",
         "globby": "^11.0.3",
         "is-glob": "^4.0.1",
@@ -1575,12 +1575,12 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "4.31.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.0.tgz",
-      "integrity": "sha512-HUcRp2a9I+P21+O21yu3ezv3GEPGjyGiXoEUQwZXjR8UxRApGeLyWH4ZIIUSalE28aG4YsV6GjtaAVB3QKOu0w==",
+      "version": "4.31.2",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.2.tgz",
+      "integrity": "sha512-PrBId7EQq2Nibns7dd/ch6S6/M4/iwLM9McbgeEbCXfxdwRUNxJ4UNreJ6Gh3fI2GNKNrWnQxKL7oCPmngKBug==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.31.0",
+        "@typescript-eslint/types": "4.31.2",
         "eslint-visitor-keys": "^2.0.0"
       }
     },
@@ -8071,9 +8071,9 @@
       "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
     },
     "react-hook-form": {
-      "version": "7.15.3",
-      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.15.3.tgz",
-      "integrity": "sha512-z30aZoEHkWE8oZvad4OcYSBI0kQua/T5sFGH9tB2HfeykFnP/pGXNap8lDio4/U1yPj2ffpbvRIvqKd/6jjBVA=="
+      "version": "7.15.4",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.15.4.tgz",
+      "integrity": "sha512-jEtsDBPfpkz1uuJVlTLDOg+jO3cG9pFHT3g5uayVvlNT551IetXE1iwrSaxUR/QPWyJA2FLx4Q/VjO2viZNfLg=="
     },
     "react-is": {
       "version": "16.13.1",

+ 4 - 4
esp/src/package.json

@@ -33,7 +33,7 @@
   },
   "main": "src/stub.js",
   "dependencies": {
-    "@fluentui/react": "^8.34.0",
+    "@fluentui/react": "^8.34.4",
     "@fluentui/react-cards": "1.0.0-beta.0",
     "@fluentui/react-hooks": "^8.3.1",
     "@fluentui/react-icons-mdl2": "^1.2.1",
@@ -68,7 +68,7 @@
     "query-string": "6.13.2",
     "react": "^16.12.0",
     "react-dom": "^16.13.1",
-    "react-hook-form": "^7.15.3",
+    "react-hook-form": "^7.15.4",
     "react-reflex": "^4.0.3",
     "react-sizeme": "^3.0.2",
     "universal-router": "^9.1.0"
@@ -77,8 +77,8 @@
     "@types/dojo": "^1.9.43",
     "@types/react": "^16.14.15",
     "@types/react-dom": "^16.9.14",
-    "@typescript-eslint/eslint-plugin": "^4.31.0",
-    "@typescript-eslint/parser": "^4.31.0",
+    "@typescript-eslint/eslint-plugin": "^4.31.2",
+    "@typescript-eslint/parser": "^4.31.2",
     "braces": ">=2.3.1",
     "cpx": "^1.5.0",
     "css-loader": "^3.4.2",

+ 10 - 22
esp/src/src-react/components/Activities.tsx

@@ -4,10 +4,11 @@ import { useConst } from "@fluentui/react-hooks";
 import * as ESPActivity from "src/ESPActivity";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { ReflexContainer, ReflexElement, ReflexSplitter, classNames, styles } from "../layouts/react-reflex";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector, tree } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector, tree } from "./DojoGrid";
 import { Summary } from "./DiskUsage";
 
 declare const dojoConfig;
@@ -51,15 +52,13 @@ interface ActivitiesProps {
 export const Activities: React.FunctionComponent<ActivitiesProps> = ({
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
     const activity = useConst(ESPActivity.Get());
-    const gridParams = useConst({
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
         store: activity.getStore({}),
-        query: {},
+        filename: "activities",
         columns: {
             col1: selector({
                 width: 27,
@@ -145,20 +144,13 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
         }
     });
 
-    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]);
+    }, [activity, refreshTable]);
 
     //  Command Bar  ---
     const wuPriority = React.useCallback((priority) => {
@@ -354,10 +346,6 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
         },
     ], [activity, refreshTable, selection, uiState.clusterNotPausedSelected, uiState.clusterPausedSelected, uiState.thorClusterSelected, uiState.wuCanDown, uiState.wuCanHigh, uiState.wuCanLow, uiState.wuCanNormal, uiState.wuCanUp, uiState.wuSelected, wuPriority]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "activities.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -403,9 +391,9 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
 
     if (dojoConfig.isContainer) {
         return <HolyGrail
-            header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+            header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
             main={
-                <DojoGrid type="Sel" store={gridParams.store} query={gridParams.query} columns={gridParams.columns} setGrid={setGrid} setSelection={setSelection} />
+                <Grid />
             }
         />;
     }
@@ -418,9 +406,9 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
         </ReflexSplitter>
         <ReflexElement>
             <HolyGrail
-                header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+                header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
                 main={
-                    <DojoGrid type="Sel" store={gridParams.store} query={gridParams.query} columns={gridParams.columns} setGrid={setGrid} setSelection={setSelection} />
+                    <Grid />
                 }
             />
         </ReflexElement>

+ 53 - 67
esp/src/src-react/components/DFUWorkunits.tsx

@@ -1,17 +1,17 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 import * as domClass from "dojo/dom-class";
 import * as ESPDFUWorkunit from "src/ESPDFUWorkunit";
 import * as FileSpray from "src/FileSpray";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
 import { Filter } from "./forms/Filter";
 import { Fields } from "./forms/Fields";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 const FilterFields: Fields = {
     "Type": { type: "checkbox", label: nlsHPCC.ArchivedOnly },
@@ -52,72 +52,66 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
     store
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [showFilter, setShowFilter] = React.useState(false);
     const [mine, setMine] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(store || ESPDFUWorkunit.CreateWUQueryStore({}));
-    const gridQuery = useConst(formatQuery(filter));
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        isProtected: {
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("locked.png", nlsHPCC.Protected);
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: store || ESPDFUWorkunit.CreateWUQueryStore({}),
+        query: formatQuery(filter),
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "dfuworkunits",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            isProtected: {
+                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 "";
+                }
             },
-            width: 25,
-            sortable: false,
-            formatter: function (_protected) {
-                if (_protected === true) {
-                    return Utility.getImageHTML("locked.png");
+            ID: {
+                label: nlsHPCC.ID,
+                width: 180,
+                formatter: function (ID, idx) {
+                    const wu = ESPDFUWorkunit.Get(ID);
+                    return `<img src='${wu.getStateImage()}'>&nbsp;<a href='#/dfuworkunits/${ID}' class='dgrid-row-url'>${ID}</a>`;
                 }
-                return "";
-            }
-        },
-        ID: {
-            label: nlsHPCC.ID,
-            width: 180,
-            formatter: function (ID, idx) {
-                const wu = ESPDFUWorkunit.Get(ID);
-                return `<img src='${wu.getStateImage()}'>&nbsp;<a href='#/dfuworkunits/${ID}' class='dgrid-row-url'>${ID}</a>`;
-            }
-        },
-        Command: {
-            label: nlsHPCC.Type,
-            width: 117,
-            formatter: function (command) {
-                if (command in FileSpray.CommandMessages) {
-                    return FileSpray.CommandMessages[command];
+            },
+            Command: {
+                label: nlsHPCC.Type,
+                width: 117,
+                formatter: function (command) {
+                    if (command in FileSpray.CommandMessages) {
+                        return FileSpray.CommandMessages[command];
+                    }
+                    return "Unknown";
+                }
+            },
+            User: { label: nlsHPCC.Owner, width: 90 },
+            JobName: { label: nlsHPCC.JobName, width: 500 },
+            ClusterName: { label: nlsHPCC.Cluster, width: 126 },
+            StateMessage: { label: nlsHPCC.State, width: 72 },
+            PercentDone: {
+                label: nlsHPCC.PctComplete, width: 90, sortable: false,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.valueCleanUp(value);
                 }
-                return "Unknown";
-            }
-        },
-        User: { label: nlsHPCC.Owner, width: 90 },
-        JobName: { label: nlsHPCC.JobName, width: 500 },
-        ClusterName: { label: nlsHPCC.Cluster, width: 126 },
-        StateMessage: { label: nlsHPCC.State, width: 72 },
-        PercentDone: {
-            label: nlsHPCC.PctComplete, width: 90, sortable: false,
-            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", formatQuery(filter));
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [filter, grid]);
-
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -175,20 +169,12 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
         },
     ], [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 = {};
     for (const field in FilterFields) {
         filterFields[field] = { ...FilterFields[field], value: filter[field] };
     }
 
-    React.useEffect(() => {
-        refreshTable();
-    }, [filter, refreshTable]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -210,10 +196,10 @@ export const DFUWorkunits: React.FunctionComponent<DFUWorkunitsProps> = ({
     }, [selection]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
             <>
-                <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
             </>
         }

+ 14 - 20
esp/src/src-react/components/FileBlooms.tsx

@@ -3,9 +3,9 @@ import { useConst } from "@fluentui/react-hooks";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 
 interface FileBloomsProps {
     cluster?: string;
@@ -18,31 +18,25 @@ export const FileBlooms: React.FunctionComponent<FileBloomsProps> = ({
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [, setSelection] = React.useState([]);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("FieldNames")));
-    const gridSort = useConst([{ attribute: "FieldNames", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        FieldNames: { label: nlsHPCC.FieldNames, sortable: true, },
-        Limit: { label: nlsHPCC.Limit, sortable: true, },
-        Probability: { label: nlsHPCC.Probability, sortable: true, },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("FieldNames")));
+    const [Grid, , refreshTable] = useGrid({
+        store,
+        sort: [{ attribute: "FieldNames", "descending": false }],
+        filename: "fileBlooms",
+        columns: {
+            FieldNames: { label: nlsHPCC.FieldNames, sortable: true, },
+            Limit: { label: nlsHPCC.Limit, sortable: true, },
+            Probability: { label: nlsHPCC.Probability, sortable: true, }
         }
-    }, [grid, gridQuery]);
+    });
 
     React.useEffect(() => {
         if (file?.Blooms) {
             const fileBlooms = file?.Blooms?.DFUFileBloom;
             if (fileBlooms) {
-                gridStore.setData(fileBlooms.map(bloom => {
+                store.setData(fileBlooms.map(bloom => {
                     return {
                         ...bloom,
                         FieldNames: bloom?.FieldNames?.Item[0] || "",
@@ -51,11 +45,11 @@ export const FileBlooms: React.FunctionComponent<FileBloomsProps> = ({
                 refreshTable();
             }
         }
-    }, [file?.Blooms, gridStore, refreshTable]);
+    }, [file?.Blooms, store, refreshTable]);
 
     return <HolyGrail
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 21 - 31
esp/src/src-react/components/FileDetailsGraph.tsx

@@ -5,10 +5,11 @@ import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 function getStateImageName(row) {
     if (row.Complete) {
@@ -36,34 +37,27 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
-    // const [helpers] = useWorkunitHelpers(wuid);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Name")));
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Name: {
-            label: nlsHPCC.Name, sortable: true,
-            formatter: function (Name, row) {
-                return Utility.getImageHTML(getStateImageName(row)) + `&nbsp;<a href='#/workunits/${file?.Wuid}/graphs/${Name}' onClick='return false;' class='dgrid-row-url'>${Name}</a>`;
+    const store = useConst(new Observable(new Memory("Name")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        filename: "graphs",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Name: {
+                label: nlsHPCC.Name, sortable: true,
+                formatter: function (Name, row) {
+                    return Utility.getImageHTML(getStateImageName(row)) + `&nbsp;<a href='#/workunits/${file?.Wuid}/graphs/${Name}' onClick='return false;' class='dgrid-row-url'>${Name}</a>`;
+                }
             }
         }
     });
 
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [grid, gridQuery]);
-
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -85,10 +79,6 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
         }
     ], [file?.Wuid, refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "graphs.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -98,7 +88,7 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
 
     React.useEffect(() => {
         if (file?.Graphs?.ECLGraph) {
-            gridStore.setData(file?.Graphs?.ECLGraph.map(item => {
+            store.setData(file?.Graphs?.ECLGraph.map(item => {
                 return {
                     Name: item,
                     Label: "",
@@ -109,12 +99,12 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
             }));
             refreshTable();
         }
-    }, [file?.Graphs?.ECLGraph, gridStore, refreshTable]);
+    }, [file?.Graphs?.ECLGraph, refreshTable, store]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 36 - 44
esp/src/src-react/components/FileHistory.tsx

@@ -5,9 +5,9 @@ import { scopedLogger } from "@hpcc-js/util";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 import * as WsDfu from "../../src/WsDfu";
 import { ShortVerticalDivider } from "./Common";
 
@@ -24,8 +24,6 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [, setSelection] = React.useState([]);
 
     //  Command Bar  ---
     const buttons: ICommandBarItemProps[] = [
@@ -45,7 +43,7 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
                     })
                         .then(response => {
                             if (response) {
-                                gridStore.setData([]);
+                                store.setData([]);
                                 refreshTable();
                             }
                         })
@@ -57,59 +55,53 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
     ];
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Name")));
-    const gridSort = useConst([{ attribute: "Name", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        Name: { label: nlsHPCC.Name, sortable: false },
-        IP: { label: nlsHPCC.IP, sortable: false },
-        Operation: { label: nlsHPCC.Operation, sortable: false },
-        Owner: { label: nlsHPCC.Owner, sortable: false },
-        Path: { label: nlsHPCC.Path, sortable: false },
-        Timestamp: { label: nlsHPCC.TimeStamp, sortable: false },
-        Workunit: { label: nlsHPCC.Workunit, sortable: false }
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("Name")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Name", "descending": false }],
+        filename: "filehistory",
+        columns: {
+            Name: { label: nlsHPCC.Name, sortable: false },
+            IP: { label: nlsHPCC.IP, sortable: false },
+            Operation: { label: nlsHPCC.Operation, sortable: false },
+            Owner: { label: nlsHPCC.Owner, sortable: false },
+            Path: { label: nlsHPCC.Path, sortable: false },
+            Timestamp: { label: nlsHPCC.TimeStamp, sortable: false },
+            Workunit: { label: nlsHPCC.Workunit, sortable: false }
         }
-    };
+    });
 
     React.useEffect(() => {
         WsDfu.ListHistory({
             request: {
                 Name: file?.Name
             }
-        })
-            .then(response => {
-                const results = response?.ListHistoryResponse?.History?.Origin;
+        }).then(response => {
+            const results = response?.ListHistoryResponse?.History?.Origin;
 
-                if (results) {
-                    gridStore.setData(results.map(row => {
-                        return {
-                            Name: row.Name,
-                            IP: row.IP,
-                            Operation: row.Operation,
-                            Owner: row.Owner,
-                            Path: row.Path,
-                            Timestamp: row.Timestamp,
-                            Workunit: row.Workunit
-                        };
-                    }));
-                    refreshTable();
-                }
-            })
-            .catch(logger.error)
+            if (results) {
+                store.setData(results.map(row => {
+                    return {
+                        Name: row.Name,
+                        IP: row.IP,
+                        Operation: row.Operation,
+                        Owner: row.Owner,
+                        Path: row.Path,
+                        Timestamp: row.Timestamp,
+                        Workunit: row.Workunit
+                    };
+                }));
+                refreshTable();
+            }
+        }).catch(logger.error)
             ;
         // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, file?.Name]);
+    }, [store, file?.Name]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={[]} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            < Grid />
         }
     />;
 };

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

@@ -4,9 +4,9 @@ import { format as d3Format } from "@hpcc-js/common";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 
 const formatNum = d3Format(",");
 
@@ -21,34 +21,28 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [, setSelection] = React.useState([]);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Id")));
-    const gridSort = useConst([{ attribute: "Id", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        Id: { label: nlsHPCC.Part, sortable: true, },
-        Copy: { label: nlsHPCC.Copy, sortable: true, },
-        Ip: { label: nlsHPCC.IP, sortable: true, },
-        Cluster: { label: nlsHPCC.Cluster, sortable: true, },
-        PartsizeInt64: { label: nlsHPCC.Size, sortable: true, },
-        CompressedSize: { label: nlsHPCC.CompressedSize, sortable: true, },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("Id")));
+    const [Grid, _selection, refreshTable, _copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Id", "descending": false }],
+        filename: "fileParts",
+        columns: {
+            Id: { label: nlsHPCC.Part, sortable: true, },
+            Copy: { label: nlsHPCC.Copy, sortable: true, },
+            Ip: { label: nlsHPCC.IP, sortable: true, },
+            Cluster: { label: nlsHPCC.Cluster, sortable: true, },
+            PartsizeInt64: { label: nlsHPCC.Size, sortable: true, },
+            CompressedSize: { label: nlsHPCC.CompressedSize, sortable: true, },
         }
-    }, [grid, gridQuery]);
+    });
 
     React.useEffect(() => {
         if (file?.DFUFilePartsOnClusters) {
             const fileParts = file?.DFUFilePartsOnClusters?.DFUFilePartsOnCluster[0]?.DFUFileParts?.DFUPart;
             if (fileParts) {
-                gridStore.setData(fileParts.map(part => {
+                store.setData(fileParts.map(part => {
                     return {
                         Id: part.Id,
                         Copy: part.Copy,
@@ -61,11 +55,11 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
                 refreshTable();
             }
         }
-    }, [cluster, file?.DFUFilePartsOnClusters, gridStore, refreshTable]);
+    }, [cluster, file?.DFUFilePartsOnClusters, store, refreshTable]);
 
     return <HolyGrail
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 82 - 96
esp/src/src-react/components/Files.tsx

@@ -1,18 +1,18 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 import * as domClass from "dojo/dom-class";
 import * as put from "put-selector/put";
 import * as WsDfu from "src/WsDfu";
 import * as ESPLogicalFile from "src/ESPLogicalFile";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector, tree } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector, tree } from "./DojoGrid";
 
 const FilterFields: Fields = {
     "LogicalName": { type: "string", label: nlsHPCC.Name, placeholder: nlsHPCC.somefile },
@@ -60,103 +60,97 @@ export const Files: React.FunctionComponent<FilesProps> = ({
     store
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [showFilter, setShowFilter] = React.useState(false);
     const [mine, setMine] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(store || ESPLogicalFile.CreateLFQueryStore({}));
-    const gridQuery = useConst(formatQuery(filter));
-    const gridSort = useConst([{ attribute: "Modified", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            disabled: function (item) {
-                return item ? item.__hpcc_isDir : true;
-            },
-            selectorType: "checkbox"
-        }),
-        IsProtected: {
-            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");
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: store || ESPLogicalFile.CreateLFQueryStore({}),
+        query: formatQuery(filter),
+        sort: [{ attribute: "Modified", "descending": true }],
+        filename: "logicalfiles",
+        columns: {
+            col1: selector({
+                width: 27,
+                disabled: function (item) {
+                    return item ? item.__hpcc_isDir : true;
+                },
+                selectorType: "checkbox"
+            }),
+            IsProtected: {
+                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 "";
                 }
-                return "";
-            }
-        },
-        IsCompressed: {
-            width: 25, sortable: false,
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("compressed.png", nlsHPCC.Compressed);
             },
-            formatter: function (compressed) {
-                if (compressed === true) {
-                    return Utility.getImageHTML("compressed.png");
-                }
-                return "";
-            }
-        },
-        __hpcc_displayName: tree({
-            label: nlsHPCC.LogicalName, width: 600,
-            formatter: function (name, row) {
-                if (row.__hpcc_isDir) {
-                    return name;
+            IsCompressed: {
+                width: 25, sortable: false,
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("compressed.png", nlsHPCC.Compressed);
+                },
+                formatter: function (compressed) {
+                    if (compressed === true) {
+                        return Utility.getImageHTML("compressed.png");
+                    }
+                    return "";
                 }
-                return (row.getStateImageHTML ? row.getStateImageHTML() + "&nbsp;" : "") + "<a href='#/files/" + row.NodeGroup + "/" + name + "' class='dgrid-row-url'>" + name + "</a>";
             },
-            renderExpando: function (level, hasChildren, expanded, object) {
-                const dir = this.grid.isRTL ? "right" : "left";
-                let cls = ".dgrid-expando-icon";
-                if (hasChildren) {
-                    cls += ".ui-icon.ui-icon-triangle-1-" + (expanded ? "se" : "e");
+            __hpcc_displayName: tree({
+                label: nlsHPCC.LogicalName, width: 600,
+                formatter: function (name, row) {
+                    if (row.__hpcc_isDir) {
+                        return name;
+                    }
+                    return (row.getStateImageHTML ? row.getStateImageHTML() + "&nbsp;" : "") + "<a href='#/files/" + row.NodeGroup + "/" + name + "' class='dgrid-row-url'>" + name + "</a>";
+                },
+                renderExpando: function (level, hasChildren, expanded, object) {
+                    const dir = this.grid.isRTL ? "right" : "left";
+                    let cls = ".dgrid-expando-icon";
+                    if (hasChildren) {
+                        cls += ".ui-icon.ui-icon-triangle-1-" + (expanded ? "se" : "e");
+                    }
+                    //@ts-ignore
+                    const node = put("div" + cls + "[style=margin-" + dir + ": " + (level * (this.indentWidth || 9)) + "px; float: " + dir + (!object.__hpcc_isDir && level === 0 ? ";display: none" : "") + "]");
+                    node.innerHTML = "&nbsp;";
+                    return node;
                 }
-                //@ts-ignore
-                const node = put("div" + cls + "[style=margin-" + dir + ": " + (level * (this.indentWidth || 9)) + "px; float: " + dir + (!object.__hpcc_isDir && level === 0 ? ";display: none" : "") + "]");
-                node.innerHTML = "&nbsp;";
-                return node;
-            }
-        }),
-        Owner: { label: nlsHPCC.Owner, width: 75 },
-        SuperOwners: { label: nlsHPCC.SuperOwner, width: 150 },
-        Description: { label: nlsHPCC.Description, width: 150 },
-        NodeGroup: { label: nlsHPCC.Cluster, width: 108 },
-        RecordCount: {
-            label: nlsHPCC.Records, width: 85,
-            renderCell: function (object, value, node, options) {
-                domClass.add(node, "justify-right");
-                node.innerText = Utility.valueCleanUp(value);
+            }),
+            Owner: { label: nlsHPCC.Owner, width: 75 },
+            SuperOwners: { label: nlsHPCC.SuperOwner, width: 150 },
+            Description: { label: nlsHPCC.Description, width: 150 },
+            NodeGroup: { label: nlsHPCC.Cluster, width: 108 },
+            RecordCount: {
+                label: nlsHPCC.Records, width: 85,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.valueCleanUp(value);
+                },
             },
-        },
-        IntSize: {
-            label: nlsHPCC.Size, width: 100,
-            renderCell: function (object, value, node, options) {
-                domClass.add(node, "justify-right");
-                node.innerText = Utility.convertedSize(value);
+            IntSize: {
+                label: nlsHPCC.Size, width: 100,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.convertedSize(value);
+                },
             },
-        },
-        Parts: {
-            label: nlsHPCC.Parts, width: 60,
-            renderCell: function (object, value, node, options) {
-                domClass.add(node, "justify-right");
-                node.innerText = Utility.valueCleanUp(value);
+            Parts: {
+                label: nlsHPCC.Parts, width: 60,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.valueCleanUp(value);
+                },
             },
-        },
-        Modified: { label: nlsHPCC.ModifiedUTCGMT, width: 162 }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", formatQuery(filter));
-        if (clearSelection) {
-            grid?.clearSelection();
+            Modified: { label: nlsHPCC.ModifiedUTCGMT, width: 162 }
         }
-    }, [filter, grid]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -202,20 +196,12 @@ export const Files: React.FunctionComponent<FilesProps> = ({
         },
     ], [mine, refreshTable, selection, store, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "logicalfiles.csv")
-    ], [grid, selection]);
-
     //  Filter  ---
     const filterFields: Fields = {};
     for (const field in FilterFields) {
         filterFields[field] = { ...FilterFields[field], value: filter[field] };
     }
 
-    React.useEffect(() => {
-        refreshTable();
-    }, [filter, refreshTable]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -228,10 +214,10 @@ export const Files: React.FunctionComponent<FilesProps> = ({
     }, [selection]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
             <>
-                <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
             </>
         }

+ 36 - 45
esp/src/src-react/components/Helpers.tsx

@@ -7,10 +7,11 @@ import * as ESPRequest from "src/ESPRequest";
 import { Memory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HelperRow, useWorkunitHelpers } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 function canShowContent(type: string) {
     switch (type) {
@@ -100,50 +101,44 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     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>`;
+    const store = useConst(new Observable(new Memory("id")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        filename: "helpers",
+        columns: {
+            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;
                 }
-                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);
             },
+            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 = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -197,10 +192,6 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
 
     ], [refreshTable, selection, uiState.canShowContent, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "logicalfiles.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -215,14 +206,14 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        gridStore.setData(helpers);
+        store.setData(helpers);
         refreshTable();
-    }, [gridStore, helpers, refreshTable]);
+    }, [store, helpers, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 48 - 58
esp/src/src-react/components/InfoGrid.tsx

@@ -6,10 +6,9 @@ import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitExceptions } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
-import { createCopyDownloadSelection } from "./Common";
 
 function extractGraphInfo(msg) {
     const retVal: { graphID?: string, subgraphID?: string, activityID?: string, activityName?: string } = {};
@@ -47,9 +46,7 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
     const [infoChecked, setInfoChecked] = React.useState(true);
     const [otherChecked, setOtherChecked] = React.useState(true);
     const [filterCounts, setFilterCounts] = React.useState<any>({});
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [exceptions] = useWorkunitExceptions(wuid);
-    const [selection, setSelection] = React.useState([]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -59,60 +56,53 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
         { 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 = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "errorwarnings.csv")
-    ], [grid, selection]);
-
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("id")));
-    const gridColumns = useConst({
-        Severity: {
-            label: nlsHPCC.Severity, field: "", width: 72, sortable: false,
-            renderCell: function (object, value, node, options) {
-                switch (value) {
-                    case "Error":
-                        domClass.add(node, "ErrorCell");
-                        break;
-                    case "Alert":
-                        domClass.add(node, "AlertCell");
-                        break;
-                    case "Warning":
-                        domClass.add(node, "WarningCell");
-                        break;
+    const store = useConst(new Observable(new Memory("id")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        filename: "errorwarnings",
+        columns: {
+            Severity: {
+                label: nlsHPCC.Severity, field: "", width: 72, sortable: false,
+                renderCell: function (object, value, node, options) {
+                    switch (value) {
+                        case "Error":
+                            domClass.add(node, "ErrorCell");
+                            break;
+                        case "Alert":
+                            domClass.add(node, "AlertCell");
+                            break;
+                        case "Warning":
+                            domClass.add(node, "WarningCell");
+                            break;
+                    }
+                    node.innerText = value;
                 }
-                node.innerText = value;
-            }
-        },
-        Source: { label: nlsHPCC.Source, field: "", width: 144, sortable: false },
-        Code: { label: nlsHPCC.Code, field: "", width: 45, sortable: false },
-        Message: {
-            label: nlsHPCC.Message, field: "",
-            sortable: false,
-            formatter: function (Message, idx) {
-                const info = extractGraphInfo(Message);
-                if (info.graphID && info.subgraphID && info.activityID) {
-                    const txt = "Graph " + info.graphID + "[" + info.subgraphID + "], " + info.activityName + "[" + info.activityID + "]";
-                    Message = Message.replace(txt, "<a href='#' onClick='return false;' class='dgrid-row-url'>" + txt + "</a>");
-                } else if (info.graphID && info.subgraphID) {
-                    const txt = "Graph " + info.graphID + "[" + info.subgraphID + "]";
-                    Message = Message.replace(txt, "<a href='#' onClick='return false;' class='dgrid-row-url'>" + txt + "</a>");
-                } else {
-                    Message = Utility.xmlEncode2(Message);
+            },
+            Source: { label: nlsHPCC.Source, field: "", width: 144, sortable: false },
+            Code: { label: nlsHPCC.Code, field: "", width: 45, sortable: false },
+            Message: {
+                label: nlsHPCC.Message, field: "",
+                sortable: false,
+                formatter: function (Message, idx) {
+                    const info = extractGraphInfo(Message);
+                    if (info.graphID && info.subgraphID && info.activityID) {
+                        const txt = "Graph " + info.graphID + "[" + info.subgraphID + "], " + info.activityName + "[" + info.activityID + "]";
+                        Message = Message.replace(txt, "<a href='#' onClick='return false;' class='dgrid-row-url'>" + txt + "</a>");
+                    } else if (info.graphID && info.subgraphID) {
+                        const txt = "Graph " + info.graphID + "[" + info.subgraphID + "]";
+                        Message = Message.replace(txt, "<a href='#' onClick='return false;' class='dgrid-row-url'>" + txt + "</a>");
+                    } else {
+                        Message = Utility.xmlEncode2(Message);
+                    }
+                    return Message;
                 }
-                return Message;
-            }
-        },
-        Column: { label: nlsHPCC.Col, field: "", width: 36, sortable: false },
-        LineNo: { label: nlsHPCC.Line, field: "", width: 36, sortable: false },
-        FileName: { label: nlsHPCC.FileName, field: "", width: 360, sortable: false }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
+            },
+            Column: { label: nlsHPCC.Col, field: "", width: 36, sortable: false },
+            LineNo: { label: nlsHPCC.Line, field: "", width: 36, sortable: false },
+            FileName: { label: nlsHPCC.FileName, field: "", width: 360, sortable: false }
         }
-    }, [grid]);
+    });
 
     React.useEffect(() => {
         const filterCounts = {
@@ -169,15 +159,15 @@ export const InfoGrid: React.FunctionComponent<InfoGridProps> = ({
             }
             return l.Severity.localeCompare(r.Severity);
         });
-        gridStore.setData(filteredExceptions);
+        store.setData(filteredExceptions);
         refreshTable();
         setFilterCounts(filterCounts);
-    }, [errorChecked, exceptions, gridStore, infoChecked, otherChecked, refreshTable, warningChecked]);
+    }, [errorChecked, exceptions, store, infoChecked, otherChecked, refreshTable, warningChecked]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid type={"SimpleGrid"} store={gridStore} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 99 - 114
esp/src/src-react/components/LandingZone.tsx

@@ -9,10 +9,11 @@ import * as FileSpray from "src/FileSpray";
 import * as ESPRequest from "src/ESPRequest";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector, tree } from "./DojoGrid";
+import { selector, tree } from "./DojoGrid";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
 import { AddFileForm } from "./forms/landing-zone/AddFileForm";
@@ -24,12 +25,24 @@ import { VariableImportForm } from "./forms/landing-zone/VariableImportForm";
 import { XmlImportForm } from "./forms/landing-zone/XmlImportForm";
 import { FileListForm } from "./forms/landing-zone/FileListForm";
 
-function formatQuery(_filter) {
+function formatQuery(targetDropzones, filter) {
+    const dropzones = targetDropzones.filter(row => row.Name === filter?.DropZoneName);
+    const machines = targetDropzones[0]?.TpMachines?.TpMachine?.filter(row => row.ConfigNetaddress === filter?.Server);
     return {
-        DropZoneName: _filter.DropZoneName,
-        Server: _filter.Server,
-        NameFilter: _filter.NameFilter,
-        ECLWatchVisibleOnly: true
+        id: "*",
+        filter: (filter?.DropZoneName && dropzones.length && machines.length) ? {
+            filter: {
+                DropZoneName: filter.DropZoneName,
+                Server: filter.Server,
+                NameFilter: filter.NameFilter,
+                ECLWatchVisibleOnly: true
+            },
+            ECLWatchVisibleOnly: true,
+            __dropZone: {
+                ...targetDropzones.filter(row => row.Name === filter?.DropZoneName)[0],
+                machine: machines[0]
+            }
+        } : undefined
     };
 }
 
@@ -51,15 +64,12 @@ const emptyFilter: LandingZoneFilter = {};
 
 interface LandingZoneProps {
     filter?: LandingZoneFilter;
-    store?: any;
 }
 
 export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
-    filter = emptyFilter,
-    store
+    filter = emptyFilter
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [showFilter, setShowFilter] = React.useState(false);
     const [showAddFile, setShowAddFile] = React.useState(false);
     const [showFixed, setShowFixed] = React.useState(false);
@@ -68,7 +78,6 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
     const [showJson, setShowJson] = React.useState(false);
     const [showVariable, setShowVariable] = React.useState(false);
     const [showBlob, setShowBlob] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [showDropZone, setShowDropzone] = React.useState(false);
     const [uploadFiles, setUploadFiles] = React.useState([]);
     const [showFileUpload, setShowFileUpload] = React.useState(false);
@@ -81,84 +90,74 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
     }, []);
 
     //  Grid ---
-    const gridStore = useConst(FileSpray.CreateLandingZonesStore({}));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "modifiedtime", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            disabled: function (item) {
-                if (item.type) {
-                    switch (item.type) {
-                        case "dropzone":
-                        case "folder":
-                        case "machine":
-                            return true;
-                    }
-                }
-                return false;
-            },
-            selectorType: "checkbox"
-        }),
-        displayName: tree({
-            label: nlsHPCC.Name,
-            sortable: false,
-            formatter: function (_name, row) {
-                let img = "";
-                let name = row.displayName;
-                if (row.isDir === undefined) {
-                    img = Utility.getImageHTML("server.png");
-                    name += " [" + row.Path + "]";
-                } else if (row.isMachine) {
-                    img = Utility.getImageHTML("machine.png");
-                } else if (row.isDir) {
-                    img = Utility.getImageHTML("folder.png");
-                } else {
-                    img = Utility.getImageHTML("file.png");
-                }
-                return img + "&nbsp;" + name;
-            },
-            renderExpando: function (level, hasChildren, expanded, object) {
-                const dir = this.grid.isRTL ? "right" : "left";
-                let cls = ".dgrid-expando-icon";
-                if (hasChildren) {
-                    cls += ".ui-icon.ui-icon-triangle-1-" + (expanded ? "se" : "e");
-                }
-                //@ts-ignore
-                const node = put("div" + cls + "[style=margin-" + dir + ": " + (level * (this.indentWidth || 9)) + "px; float: " + dir + "; margin-top: 3px]");
-                node.innerHTML = "&nbsp;";
-                return node;
+    const store = useConst(FileSpray.CreateLandingZonesStore({}));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        query: formatQuery(targetDropzones, filter),
+        sort: [{ attribute: "modifiedtime", "descending": true }],
+        filename: "landingZones",
+        getSelected: function () {
+            if (filter?.__dropZone) {
+                return this.inherited(arguments, [FileSpray.CreateLandingZonesFilterStore({})]);
             }
-        }),
-        filesize: {
-            label: nlsHPCC.Size, width: 100,
-            renderCell: function (object, value, node, options) {
-                domClass.add(node, "justify-right");
-                node.innerText = Utility.convertedSize(value);
-            },
+            return this.inherited(arguments, [FileSpray.CreateFileListStore({})]);
         },
-        modifiedtime: { label: nlsHPCC.Date, width: 162 }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        const dropzones = targetDropzones.filter(row => row.Name === filter?.DropZoneName);
-        const machines = targetDropzones[0]?.TpMachines?.TpMachine?.filter(row => row.ConfigNetaddress === filter?.Server);
-        const query = {
-            id: "*",
-            filter: (filter?.DropZoneName && dropzones.length && machines.length) ? {
-                ...formatQuery(filter),
-                ECLWatchVisibleOnly: true,
-                __dropZone: {
-                    ...targetDropzones.filter(row => row.Name === filter?.DropZoneName)[0],
-                    machine: machines[0]
+        columns: {
+            col1: selector({
+                width: 27,
+                disabled: function (item) {
+                    if (item.type) {
+                        switch (item.type) {
+                            case "dropzone":
+                            case "folder":
+                            case "machine":
+                                return true;
+                        }
+                    }
+                    return false;
+                },
+                selectorType: "checkbox"
+            }),
+            displayName: tree({
+                label: nlsHPCC.Name,
+                sortable: false,
+                formatter: function (_name, row) {
+                    let img = "";
+                    let name = row.displayName;
+                    if (row.isDir === undefined) {
+                        img = Utility.getImageHTML("server.png");
+                        name += " [" + row.Path + "]";
+                    } else if (row.isMachine) {
+                        img = Utility.getImageHTML("machine.png");
+                    } else if (row.isDir) {
+                        img = Utility.getImageHTML("folder.png");
+                    } else {
+                        img = Utility.getImageHTML("file.png");
+                    }
+                    return img + "&nbsp;" + name;
+                },
+                renderExpando: function (level, hasChildren, expanded, object) {
+                    const dir = this.grid.isRTL ? "right" : "left";
+                    let cls = ".dgrid-expando-icon";
+                    if (hasChildren) {
+                        cls += ".ui-icon.ui-icon-triangle-1-" + (expanded ? "se" : "e");
+                    }
+                    //@ts-ignore
+                    const node = put("div" + cls + "[style=margin-" + dir + ": " + (level * (this.indentWidth || 9)) + "px; float: " + dir + "; margin-top: 3px]");
+                    node.innerHTML = "&nbsp;";
+                    return node;
                 }
-            } : undefined
-        };
-        grid?.set("query", query);
-        if (clearSelection) {
-            grid?.clearSelection();
+            }),
+            filesize: {
+                label: nlsHPCC.Size, width: 100,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.convertedSize(value);
+                },
+            },
+            modifiedtime: { label: nlsHPCC.Date, width: 162 }
         }
-    }, [filter, grid, targetDropzones]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -200,7 +199,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
                 if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
                     selection.forEach((item, idx) => {
                         if (item._isUserFile) {
-                            gridStore.removeUserFile(item);
+                            store.removeUserFile(item);
                             refreshTable(true);
                         } else {
                             FileSpray.DeleteDropZoneFile({
@@ -221,12 +220,12 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
         },
         { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
-            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
+            key: "filter", text: nlsHPCC.Filter, iconProps: { iconName: "Filter" },
             onClick: () => setShowFilter(true)
         },
         { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
-            key: "addFile", text: nlsHPCC.AddFile, disabled: !!store,
+            key: "addFile", text: nlsHPCC.AddFile,
             onClick: () => setShowAddFile(true)
         },
         { key: "divider_5", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
@@ -255,12 +254,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
             onClick: () => setShowBlob(true)
         },
         { key: "divider_6", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> }
-    ], [gridStore, refreshTable, selection, store]);
-
-    React.useEffect(() => {
-        //  refreshTable changes when filter changes...
-        refreshTable();
-    }, [refreshTable]);
+    ], [store, refreshTable, selection]);
 
     //  Filter  ---
     const filterFields: Fields = {};
@@ -268,7 +262,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
         filterFields[field] = { ...FilterFields[field], value: filter[field] };
     }
 
-    const dropStyles = mergeStyleSets({
+    const dropStyles = React.useMemo(() => mergeStyleSets({
         dzWrapper: {
             position: "absolute",
             top: "118px",
@@ -300,7 +294,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
         displayNone: {
             display: "none"
         }
-    });
+    }), [showDropZone]);
 
     const handleFileDragEnter = React.useCallback((evt) => {
         evt.preventDefault();
@@ -333,7 +327,7 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
     }, [setShowFileUpload, setUploadFiles]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
             <>
                 <input
@@ -345,29 +339,20 @@ export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
                         <p>Drop file(s) to upload.</p>
                     </div>
                 </div>
-                <DojoGrid
-                    store={gridStore} columns={gridColumns} query={gridQuery}
-                    getSelected={function () {
-                        if (filter?.__dropZone) {
-                            return this.inherited(arguments, [FileSpray.CreateLandingZonesFilterStore({})]);
-                        }
-                        return this.inherited(arguments, [FileSpray.CreateFileListStore({})]);
-                    }}
-                    sort={gridSort} setGrid={setGrid} setSelection={setSelection}
-                />
+                <Grid />
                 <Filter
                     showFilter={showFilter} setShowFilter={setShowFilter}
                     filterFields={filterFields} onApply={pushParams}
                 />
-                { uploadFiles &&
-                <FileListForm
-                    formMinWidth={360} selection={uploadFiles}
-                    showForm={showFileUpload} setShowForm={setShowFileUpload}
-                    onSubmit={refreshTable}
-                />
+                {uploadFiles &&
+                    <FileListForm
+                        formMinWidth={360} selection={uploadFiles}
+                        showForm={showFileUpload} setShowForm={setShowFileUpload}
+                        onSubmit={() => refreshTable()}
+                    />
                 }
                 <AddFileForm
-                    formMinWidth={620} store={gridStore} refreshGrid={refreshTable}
+                    formMinWidth={620} store={store} refreshGrid={refreshTable}
                     showForm={showAddFile} setShowForm={setShowAddFile}
                 />
                 <FixedImportForm

+ 15 - 25
esp/src/src-react/components/LogViewer.tsx

@@ -4,9 +4,8 @@ import { useConst } from "@fluentui/react-hooks";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
-import { createCopyDownloadSelection } from "./Common";
 import { useECLWatchLogger } from "../hooks/logging";
 import { Level } from "@hpcc-js/util";
 
@@ -21,9 +20,7 @@ export const LogViewer: React.FunctionComponent<LogViewerProps> = ({
     const [infoChecked, setInfoChecked] = React.useState(true);
     const [otherChecked, setOtherChecked] = React.useState(true);
     const [filterCounts, setFilterCounts] = React.useState<any>({});
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [log, lastUpdate] = useECLWatchLogger();
-    const [selection, setSelection] = React.useState([]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -33,25 +30,18 @@ export const LogViewer: React.FunctionComponent<LogViewerProps> = ({
         { 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 = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "errorwarnings.csv")
-    ], [grid, selection]);
-
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("dateTime")));
-    const gridColumns = useConst({
-        dateTime: { label: nlsHPCC.Time, width: 160, sortable: false },
-        level: { label: nlsHPCC.Severity, width: 112, sortable: false, formatter: level => Level[level].toUpperCase() },
-        id: { label: nlsHPCC.Source, width: 212, sortable: false },
-        message: { label: nlsHPCC.Message, sortable: false }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("dateTime")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        filename: "errorwarnings",
+        columns: {
+            dateTime: { label: nlsHPCC.Time, width: 160, sortable: false },
+            level: { label: nlsHPCC.Severity, width: 112, sortable: false, formatter: level => Level[level].toUpperCase() },
+            id: { label: nlsHPCC.Source, width: 212, sortable: false },
+            message: { label: nlsHPCC.Message, sortable: false }
         }
-    }, [grid]);
+    });
 
     React.useEffect(() => {
         const filterCounts = {
@@ -93,15 +83,15 @@ export const LogViewer: React.FunctionComponent<LogViewerProps> = ({
         }).sort((l, r) => {
             return l.level - r.level;
         });
-        gridStore.setData(filteredExceptions);
+        store.setData(filteredExceptions);
         refreshTable();
         setFilterCounts(filterCounts);
-    }, [errorChecked, gridStore, infoChecked, log, otherChecked, refreshTable, warningChecked, lastUpdate]);
+    }, [errorChecked, store, infoChecked, log, otherChecked, refreshTable, warningChecked, lastUpdate]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid type={"SimpleGrid"} store={gridStore} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 23 - 28
esp/src/src-react/components/PackageMapParts.tsx

@@ -8,10 +8,11 @@ import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import * as WsPackageMaps from "src/WsPackageMaps";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { pushUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
 import { AddPackageMapPart } from "./forms/AddPackageMapPart";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 import { HolyGrail } from "../layouts/HolyGrail";
 
 const logger = scopedLogger("../components/PackageMapParts.tsx");
@@ -28,35 +29,29 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
     name,
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [_package, setPackage] = React.useState<any>(undefined);
     const [showAddPartForm, setShowAddPartForm] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     const [showError, setShowError] = React.useState(false);
     const [errorMessage, setErrorMessage] = React.useState("");
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Part")));
-    const gridSort = useConst([{ attribute: "Part", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        col1: selector({ width: 27, selectorType: "checkbox" }),
-        Part: {
-            label: nlsHPCC.Parts,
-            formatter: function (part, row) {
-                return `<a href="#/packagemaps/${name}/parts/${part}" class='dgrid-row-url'>${part}</a>`;
-            }
-        },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("Part")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Part", "descending": false }],
+        filename: "packageMapParts",
+        columns: {
+            col1: selector({ width: 27, selectorType: "checkbox" }),
+            Part: {
+                label: nlsHPCC.Parts,
+                formatter: function (part, row) {
+                    return `<a href="#/packagemaps/${name}/parts/${part}" class='dgrid-row-url'>${part}</a>`;
+                }
+            },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -83,7 +78,7 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
                         })
                             .then(({ RemovePartFromPackageMapResponse, Exceptions }) => {
                                 if (RemovePartFromPackageMapResponse?.status?.Code === 0) {
-                                    gridStore.remove(item.Part);
+                                    store.remove(item.Part);
                                     refreshTable();
                                 } else if (Exceptions?.Exception.length > 0) {
                                     setShowError(true);
@@ -109,7 +104,7 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
                 }
             }
         },
-    ], [_package, gridStore, name, refreshTable, selection, uiState.hasSelection]);
+    ], [_package, store, name, refreshTable, selection, uiState.hasSelection]);
 
     React.useEffect(() => {
         WsPackageMaps.getPackageMapById({ packageMap: name })
@@ -120,12 +115,12 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
                         Part: part.attributes[0].nodeValue
                     };
                 });
-                gridStore.setData(parts);
+                store.setData(parts);
                 refreshTable();
             })
             .catch(logger.error)
             ;
-    }, [gridStore, name, refreshTable]);
+    }, [store, name, refreshTable]);
 
     React.useEffect(() => {
         WsPackageMaps.PackageMapQuery({})
@@ -155,14 +150,14 @@ export const PackageMapParts: React.FunctionComponent<PackageMapPartsProps> = ({
         }
         <SizeMe monitorHeight>{({ size }) =>
             <HolyGrail
-                header={<CommandBar items={buttons} />}
+                header={<CommandBar items={buttons} farItems={copyButtons} />}
                 main={
-                    <DojoGrid store={gridStore} sort={gridSort} query={gridQuery} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                    <Grid />
                 }
             />
         }</SizeMe>
         <AddPackageMapPart
-            showForm={showAddPartForm} setShowForm={setShowAddPartForm} store={gridStore}
+            showForm={showAddPartForm} setShowForm={setShowAddPartForm} store={store}
             refreshTable={refreshTable} target={_package?.Target} packageMap={_package?.Id.split("::")[1]}
         />
     </>;

+ 31 - 42
esp/src/src-react/components/PackageMaps.tsx

@@ -1,18 +1,18 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, DefaultButton, Dropdown, ICommandBarItemProps, IDropdownOption, IStackTokens, Label, mergeStyleSets, MessageBar, MessageBarType, Pivot, PivotItem, Stack } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 import { scopedLogger } from "@hpcc-js/util";
 import { SizeMe } from "react-sizeme";
 import * as ESPPackageProcess from "src/ESPPackageProcess";
 import * as WsPackageMaps from "src/WsPackageMaps";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { pivotItemStyle } from "../layouts/pivot";
 import { pushParams, pushUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
 import { AddPackageMap } from "./forms/AddPackageMap";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { ReflexContainer, ReflexElement, ReflexSplitter } from "../layouts/react-reflex";
 import { TextSourceEditor, XMLSourceEditor } from "./SourceEditor";
@@ -87,8 +87,6 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
     tab = "packageMaps"
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-
     const [targets, setTargets] = React.useState<IDropdownOption[]>();
     const [processes, setProcesses] = React.useState<IDropdownOption[]>();
     const [activeMapTarget, setActiveMapTarget] = React.useState("");
@@ -98,7 +96,6 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
     const [processFilters, setProcessFilters] = React.useState<IDropdownOption[]>();
     const [showFilter, setShowFilter] = React.useState(false);
     const [showAddForm, setShowAddForm] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     const [showError, setShowError] = React.useState(false);
@@ -183,39 +180,35 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
     }, [activeMapProcess, activeMapTarget]);
 
     //  Grid ---
-    const gridStore = useConst(store || ESPPackageProcess.CreatePackageMapQueryObjectStore({}));
-    const gridQuery = useConst(formatQuery(filter));
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Id: {
-            label: nlsHPCC.PackageMap,
-            formatter: function (Id, idx) {
-                return `<a href="#/packagemaps/${Id}" class='dgrid-row-url'>${Id}</a>`;
-            }
-        },
-        Target: { label: nlsHPCC.Target },
-        Process: { label: nlsHPCC.ProcessFilter },
-        Active: {
-            label: nlsHPCC.Active,
-            formatter: function (active) {
-                if (active === true) {
-                    return "A";
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: store || ESPPackageProcess.CreatePackageMapQueryObjectStore({}),
+        query: formatQuery(filter),
+        filename: "packageMap",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Id: {
+                label: nlsHPCC.PackageMap,
+                formatter: function (Id, idx) {
+                    return `<a href="#/packagemaps/${Id}" class='dgrid-row-url'>${Id}</a>`;
                 }
-                return "";
-            }
-        },
-        Description: { label: nlsHPCC.Description }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", formatQuery(filter));
-        if (clearSelection) {
-            grid?.clearSelection();
+            },
+            Target: { label: nlsHPCC.Target },
+            Process: { label: nlsHPCC.ProcessFilter },
+            Active: {
+                label: nlsHPCC.Active,
+                formatter: function (active) {
+                    if (active === true) {
+                        return "A";
+                    }
+                    return "";
+                }
+            },
+            Description: { label: nlsHPCC.Description }
         }
-    }, [filter, grid]);
+    });
 
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -360,10 +353,6 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
             ;
     }, []);
 
-    React.useEffect(() => {
-        refreshTable();
-    }, [filter, refreshTable]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -397,10 +386,10 @@ export const PackageMaps: React.FunctionComponent<PackageMapsProps> = ({
             >
                 <PivotItem headerText={nlsHPCC.PackageMaps} itemKey="list" style={pivotItemStyle(size)} >
                     <HolyGrail
-                        header={<CommandBar items={buttons} />}
+                        header={<CommandBar items={buttons} farItems={copyButtons} />}
                         main={
                             <>
-                                <DojoGrid store={gridStore} query={gridQuery} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                                <Grid />
                                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
                             </>
                         }

+ 24 - 32
esp/src/src-react/components/ProtectedBy.tsx

@@ -4,9 +4,9 @@ import { scopedLogger } from "@hpcc-js/util";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 import * as WsDfu from "../../src/WsDfu";
 
 const logger = scopedLogger("../components/ProtectedBy.tsx");
@@ -22,52 +22,44 @@ export const ProtectedBy: React.FunctionComponent<ProtectedByProps> = ({
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [, setSelection] = React.useState([]);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("Owner")));
-    const gridSort = useConst([{ attribute: "Owner", "descending": false }]);
-    const gridQuery = useConst({});
-    const gridColumns = useConst({
-        Owner: { label: nlsHPCC.Owner, sortable: false },
-        Modified: { label: nlsHPCC.Modified, sortable: false },
-    });
-
-    const refreshTable = (clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("Owner")));
+    const [Grid, _selection, refreshTable, _copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Owner", "descending": false }],
+        filename: "protectedBy",
+        columns: {
+            Owner: { label: nlsHPCC.Owner, sortable: false },
+            Modified: { label: nlsHPCC.Modified, sortable: false },
         }
-    };
+    });
 
     React.useEffect(() => {
         WsDfu.DFUInfo({
             request: {
                 Name: file?.Name
             }
-        })
-            .then(response => {
-                const results = response?.DFUInfoResponse?.FileDetail.ProtectList.DFUFileProtect;
+        }).then(response => {
+            const results = response?.DFUInfoResponse?.FileDetail.ProtectList.DFUFileProtect;
 
-                if (results) {
-                    gridStore.setData(results.map(row => {
-                        return {
-                            Owner: row.Owner,
-                            Modified: row.Modified
-                        };
-                    }));
-                    refreshTable();
-                }
-            })
+            if (results) {
+                store.setData(results.map(row => {
+                    return {
+                        Owner: row.Owner,
+                        Modified: row.Modified
+                    };
+                }));
+                refreshTable();
+            }
+        })
             .catch(logger.error)
             ;
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [gridStore, file?.Name]);
+    }, [store, file?.Name, refreshTable]);
 
     return <HolyGrail
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 101 - 115
esp/src/src-react/components/Queries.tsx

@@ -1,16 +1,16 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 import * as WsWorkunits from "src/WsWorkunits";
 import * as ESPQuery from "src/ESPQuery";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 const FilterFields: Fields = {
     "QueryID": { type: "string", label: nlsHPCC.ID, placeholder: nlsHPCC.QueryIDPlaceholder },
@@ -57,125 +57,119 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
     store
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [showFilter, setShowFilter] = React.useState(false);
     const [mine, setMine] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(store || ESPQuery.CreateQueryStore({}));
-    const gridQuery = useConst(formatQuery(filter));
-    const gridSort = useConst([{ attribute: "Id" }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Suspended: {
-            label: nlsHPCC.Suspended,
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("suspended.png", nlsHPCC.Suspended);
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: store || ESPQuery.CreateQueryStore({}),
+        query: formatQuery(filter),
+        sort: [{ attribute: "Id" }],
+        filename: "roxiequeries",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Suspended: {
+                label: nlsHPCC.Suspended,
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("suspended.png", nlsHPCC.Suspended);
+                },
+                width: 25,
+                sortable: false,
+                formatter: function (suspended) {
+                    if (suspended === true) {
+                        return Utility.getImageHTML("suspended.png");
+                    }
+                    return "";
+                }
             },
-            width: 25,
-            sortable: false,
-            formatter: function (suspended) {
-                if (suspended === true) {
-                    return Utility.getImageHTML("suspended.png");
+            ErrorCount: {
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("errwarn.png", nlsHPCC.ErrorWarnings);
+                },
+                width: 25,
+                sortable: false,
+                formatter: function (error) {
+                    if (error > 0) {
+                        return Utility.getImageHTML("errwarn.png");
+                    }
+                    return "";
                 }
-                return "";
-            }
-        },
-        ErrorCount: {
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("errwarn.png", nlsHPCC.ErrorWarnings);
             },
-            width: 25,
-            sortable: false,
-            formatter: function (error) {
-                if (error > 0) {
-                    return Utility.getImageHTML("errwarn.png");
+            MixedNodeStates: {
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("mixwarn.png", nlsHPCC.MixedNodeStates);
+                },
+                width: 25,
+                sortable: false,
+                formatter: function (mixed) {
+                    if (mixed === true) {
+                        return Utility.getImageHTML("mixwarn.png");
+                    }
+                    return "";
                 }
-                return "";
-            }
-        },
-        MixedNodeStates: {
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("mixwarn.png", nlsHPCC.MixedNodeStates);
             },
-            width: 25,
-            sortable: false,
-            formatter: function (mixed) {
-                if (mixed === true) {
-                    return Utility.getImageHTML("mixwarn.png");
+            Activated: {
+                renderHeaderCell: function (node) {
+                    node.innerHTML = Utility.getImageHTML("active.png", nlsHPCC.Active);
+                },
+                width: 25,
+                formatter: function (activated) {
+                    if (activated === true) {
+                        return Utility.getImageHTML("active.png");
+                    }
+                    return Utility.getImageHTML("inactive.png");
                 }
-                return "";
-            }
-        },
-        Activated: {
-            renderHeaderCell: function (node) {
-                node.innerHTML = Utility.getImageHTML("active.png", nlsHPCC.Active);
             },
-            width: 25,
-            formatter: function (activated) {
-                if (activated === true) {
-                    return Utility.getImageHTML("active.png");
+            Id: {
+                label: nlsHPCC.ID,
+                width: 380,
+                formatter: function (Id, row) {
+                    return `<a href='#/queries/${row.QuerySetId}/${Id}' class='dgrid-row-url'>${Id}</a>`;
                 }
-                return Utility.getImageHTML("inactive.png");
-            }
-        },
-        Id: {
-            label: nlsHPCC.ID,
-            width: 380,
-            formatter: function (Id, row) {
-                return `<a href='#/queries/${row.QuerySetId}/${Id}' class='dgrid-row-url'>${Id}</a>`;
-            }
-        },
-        priority: {
-            label: nlsHPCC.Priority,
-            width: 80,
-            formatter: function (priority, row) {
-                return priority === undefined ? "" : priority;
-            }
-        },
-        Name: {
-            label: nlsHPCC.Name
-        },
-        QuerySetId: {
-            width: 140,
-            label: nlsHPCC.Target,
-            sortable: true
-        },
-        Wuid: {
-            width: 160,
-            label: nlsHPCC.WUID,
-            formatter: function (Wuid, idx) {
-                return "<a href='#' onClick='return false;' class='dgrid-row-url2'>" + Wuid + "</a>";
+            },
+            priority: {
+                label: nlsHPCC.Priority,
+                width: 80,
+                formatter: function (priority, row) {
+                    return priority === undefined ? "" : priority;
+                }
+            },
+            Name: {
+                label: nlsHPCC.Name
+            },
+            QuerySetId: {
+                width: 140,
+                label: nlsHPCC.Target,
+                sortable: true
+            },
+            Wuid: {
+                width: 160,
+                label: nlsHPCC.WUID,
+                formatter: function (Wuid, idx) {
+                    return "<a href='#' onClick='return false;' class='dgrid-row-url2'>" + Wuid + "</a>";
+                }
+            },
+            Dll: {
+                width: 180,
+                label: nlsHPCC.Dll
+            },
+            PublishedBy: {
+                width: 100,
+                label: nlsHPCC.PublishedBy,
+                sortable: false
+            },
+            Status: {
+                width: 100,
+                label: nlsHPCC.Status,
+                sortable: false
             }
-        },
-        Dll: {
-            width: 180,
-            label: nlsHPCC.Dll
-        },
-        PublishedBy: {
-            width: 100,
-            label: nlsHPCC.PublishedBy,
-            sortable: false
-        },
-        Status: {
-            width: 100,
-            label: nlsHPCC.Status,
-            sortable: 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[] => [
         {
@@ -236,20 +230,12 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
         },
     ], [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 = {};
     for (const field in FilterFields) {
         filterFields[field] = { ...FilterFields[field], value: filter[field] };
     }
 
-    React.useEffect(() => {
-        refreshTable();
-    }, [filter, refreshTable]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -272,10 +258,10 @@ export const Queries: React.FunctionComponent<QueriesProps> = ({
     }, [selection]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
             <>
-                <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
             </>
         }

+ 15 - 20
esp/src/src-react/components/QueryErrors.tsx

@@ -6,8 +6,8 @@ import * as ESPQuery from "src/ESPQuery";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 
 const logger = scopedLogger("../components/QueryErrors.tsx");
 
@@ -22,24 +22,19 @@ export const QueryErrors: React.FunctionComponent<QueryErrorsProps> = ({
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        Cluster: { label: nlsHPCC.Cluster, width: 140 },
-        Errors: { label: nlsHPCC.Errors },
-        State: { label: nlsHPCC.State, width: 120 },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "queryErrors",
+        columns: {
+            Cluster: { label: nlsHPCC.Cluster, width: 140 },
+            Errors: { label: nlsHPCC.Errors },
+            State: { label: nlsHPCC.State, width: 120 },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -58,7 +53,7 @@ export const QueryErrors: React.FunctionComponent<QueryErrorsProps> = ({
             .then(({ WUQueryDetailsResponse }) => {
                 const clusterStates = query?.Clusters?.ClusterQueryState;
                 if (clusterStates) {
-                    gridStore.setData(clusterStates.map((item, idx) => {
+                    store.setData(clusterStates.map((item, idx) => {
                         return {
                             __hpcc_id: idx,
                             Cluster: item.Cluster,
@@ -71,10 +66,10 @@ export const QueryErrors: React.FunctionComponent<QueryErrorsProps> = ({
             })
             .catch(logger.error)
             ;
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={null} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

+ 22 - 26
esp/src/src-react/components/QueryGraphs.tsx

@@ -7,8 +7,9 @@ import * as ESPQuery from "src/ESPQuery";
 import { Memory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 
 const logger = scopedLogger("src-react/components/QueryGraphs.tsx");
 
@@ -34,30 +35,25 @@ export const QueryGraphs: React.FunctionComponent<QueryGraphsProps> = ({
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        col1: selector({ width: 27, selectorType: "checkbox" }),
-        Name: {
-            label: nlsHPCC.Name,
-            formatter: function (Name, row) {
-                const url = `#/queries/${querySet}/${queryId}/graphs/${row.Wuid}/${Name}`;
-                return Utility.getImageHTML(getStateImageName(row)) + `&nbsp;<a href='${url}' class='dgrid-row-url'>${Name}</a>`;
-            }
-        },
-        Type: { label: nlsHPCC.Type, width: 72 },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "queryGraphs",
+        columns: {
+            col1: selector({ width: 27, selectorType: "checkbox" }),
+            Name: {
+                label: nlsHPCC.Name,
+                formatter: function (Name, row) {
+                    const url = `#/queries/${querySet}/${queryId}/graphs/${row.Wuid}/${Name}`;
+                    return Utility.getImageHTML(getStateImageName(row)) + `&nbsp;<a href='${url}' class='dgrid-row-url'>${Name}</a>`;
+                }
+            },
+            Type: { label: nlsHPCC.Type, width: 72 },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -76,7 +72,7 @@ export const QueryGraphs: React.FunctionComponent<QueryGraphsProps> = ({
             .then(({ WUQueryDetailsResponse }) => {
                 const graphs = query?.WUGraphs?.ECLGraph;
                 if (graphs) {
-                    gridStore.setData(graphs.map((item, idx) => {
+                    store.setData(graphs.map((item, idx) => {
                         return {
                             __hpcc_id: idx,
                             Name: item.Name,
@@ -92,10 +88,10 @@ export const QueryGraphs: React.FunctionComponent<QueryGraphsProps> = ({
             })
             .catch(logger.error)
             ;
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={null} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

+ 13 - 18
esp/src/src-react/components/QueryLibrariesUsed.tsx

@@ -6,8 +6,8 @@ import * as ESPQuery from "src/ESPQuery";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 
 const logger = scopedLogger("src-react/components/QueryLibrariesUsed.tsx");
 
@@ -22,22 +22,17 @@ export const QueryLibrariesUsed: React.FunctionComponent<QueryLibrariesUsedProps
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        Name: { label: nlsHPCC.LibrariesUsed }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "queryLibraries",
+        columns: {
+            Name: { label: nlsHPCC.LibrariesUsed }
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -56,7 +51,7 @@ export const QueryLibrariesUsed: React.FunctionComponent<QueryLibrariesUsedProps
             .then(({ WUQueryDetailsResponse }) => {
                 const librariesUsed = query?.LibrariesUsed?.Item;
                 if (librariesUsed) {
-                    gridStore.setData(librariesUsed.map((item, idx) => {
+                    store.setData(librariesUsed.map((item, idx) => {
                         return {
                             __hpcc_id: idx,
                             Name: item
@@ -67,10 +62,10 @@ export const QueryLibrariesUsed: React.FunctionComponent<QueryLibrariesUsedProps
             })
             .catch(logger.error)
             ;
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={null} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

+ 20 - 25
esp/src/src-react/components/QueryLogicalFiles.tsx

@@ -6,9 +6,10 @@ import * as ESPQuery from "src/ESPQuery";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushUrl } from "../util/history";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 import { ShortVerticalDivider } from "./Common";
 
 const logger = scopedLogger("../components/QueryLogicalFiles.tsx");
@@ -28,30 +29,24 @@ export const QueryLogicalFiles: React.FunctionComponent<QueryLogicalFilesProps>
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        col1: selector({ selectorType: "checkbox", width: 25 }),
-        File: {
-            label: nlsHPCC.File,
-            formatter: function (item, row) {
-                return `<a href="#/files/${querySet}/${item}">${item}</a>`;
-            }
-        },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "queryLogicalFiles",
+        columns: {
+            col1: selector({ selectorType: "checkbox", width: 25 }),
+            File: {
+                label: nlsHPCC.File,
+                formatter: function (item, row) {
+                    return `<a href="#/files/${querySet}/${item}">${item}</a>`;
+                }
+            },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -94,7 +89,7 @@ export const QueryLogicalFiles: React.FunctionComponent<QueryLogicalFilesProps>
             .then(({ WUQueryDetailsResponse }) => {
                 const logicalFiles = query?.LogicalFiles?.Item;
                 if (logicalFiles) {
-                    gridStore.setData(logicalFiles.map((item, idx) => {
+                    store.setData(logicalFiles.map((item, idx) => {
                         return {
                             __hpcc_id: idx,
                             File: item
@@ -105,10 +100,10 @@ export const QueryLogicalFiles: React.FunctionComponent<QueryLogicalFilesProps>
             })
             .catch(logger.error)
             ;
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

+ 25 - 30
esp/src/src-react/components/QuerySummaryStats.tsx

@@ -5,8 +5,8 @@ import { Query } from "@hpcc-js/comms";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { DojoGrid } from "./DojoGrid";
 
 interface QuerySummaryStatsProps {
     querySet: string;
@@ -19,34 +19,29 @@ export const QuerySummaryStats: React.FunctionComponent<QuerySummaryStatsProps>
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        Endpoint: { label: nlsHPCC.EndPoint, width: 72, sortable: true },
-        Status: { label: nlsHPCC.Status, width: 72, sortable: true },
-        StartTime: { label: nlsHPCC.StartTime, width: 160, sortable: true },
-        EndTime: { label: nlsHPCC.EndTime, width: 160, sortable: true },
-        CountTotal: { label: nlsHPCC.CountTotal, width: 88, sortable: true },
-        CountFailed: { label: nlsHPCC.CountFailed, width: 80, sortable: true },
-        AverageBytesOut: { label: nlsHPCC.MeanBytesOut, width: 80, sortable: true },
-        SizeAvgPeakMemory: { label: nlsHPCC.SizeMeanPeakMemory, width: 88, sortable: true },
-        TimeAvgTotalExecuteMinutes: { label: nlsHPCC.TimeMeanTotalExecuteMinutes, width: 88, sortable: true },
-        TimeMinTotalExecuteMinutes: { label: nlsHPCC.TimeMinTotalExecuteMinutes, width: 88, sortable: true },
-        TimeMaxTotalExecuteMinutes: { label: nlsHPCC.TimeMaxTotalExecuteMinutes, width: 88, sortable: true },
-        Percentile97: { label: nlsHPCC.Percentile97, width: 80, sortable: true },
-        Percentile97Estimate: { label: nlsHPCC.Percentile97Estimate, sortable: true }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "querySummaryStats",
+        columns: {
+            Endpoint: { label: nlsHPCC.EndPoint, width: 72, sortable: true },
+            Status: { label: nlsHPCC.Status, width: 72, sortable: true },
+            StartTime: { label: nlsHPCC.StartTime, width: 160, sortable: true },
+            EndTime: { label: nlsHPCC.EndTime, width: 160, sortable: true },
+            CountTotal: { label: nlsHPCC.CountTotal, width: 88, sortable: true },
+            CountFailed: { label: nlsHPCC.CountFailed, width: 80, sortable: true },
+            AverageBytesOut: { label: nlsHPCC.MeanBytesOut, width: 80, sortable: true },
+            SizeAvgPeakMemory: { label: nlsHPCC.SizeMeanPeakMemory, width: 88, sortable: true },
+            TimeAvgTotalExecuteMinutes: { label: nlsHPCC.TimeMeanTotalExecuteMinutes, width: 88, sortable: true },
+            TimeMinTotalExecuteMinutes: { label: nlsHPCC.TimeMinTotalExecuteMinutes, width: 88, sortable: true },
+            TimeMaxTotalExecuteMinutes: { label: nlsHPCC.TimeMaxTotalExecuteMinutes, width: 88, sortable: true },
+            Percentile97: { label: nlsHPCC.Percentile97, width: 80, sortable: true },
+            Percentile97Estimate: { label: nlsHPCC.Percentile97Estimate, sortable: true }
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -64,7 +59,7 @@ export const QuerySummaryStats: React.FunctionComponent<QuerySummaryStatsProps>
         if (!query) return;
         query?.fetchSummaryStats().then(({ StatsList }) => {
             if (StatsList?.QuerySummaryStats) {
-                gridStore.setData(StatsList?.QuerySummaryStats.map((item, idx) => {
+                store.setData(StatsList?.QuerySummaryStats.map((item, idx) => {
                     return {
                         __hpcc_id: idx,
                         Endpoint: item.Endpoint,
@@ -85,10 +80,10 @@ export const QuerySummaryStats: React.FunctionComponent<QuerySummaryStatsProps>
                 refreshTable();
             }
         });
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={null} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

+ 20 - 25
esp/src/src-react/components/QuerySuperFiles.tsx

@@ -6,10 +6,11 @@ import * as ESPQuery from "src/ESPQuery";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 
 const logger = scopedLogger("src-react/components/QuerySuperFiles.tsx");
 
@@ -28,30 +29,24 @@ export const QuerySuperFiles: React.FunctionComponent<QuerySuperFilesProps> = ({
 }) => {
 
     const [query, setQuery] = React.useState<any>();
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new Memory("__hpcc_id")));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "__hpcc_id" }]);
-    const gridColumns = useConst({
-        col1: selector({ selectorType: "checkbox", width: 25 }),
-        File: {
-            label: nlsHPCC.File,
-            formatter: function (item, row) {
-                return `<a href="#/files/${querySet}/${item}">${item}</a>`;
-            }
-        },
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new Memory("__hpcc_id")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "__hpcc_id" }],
+        filename: "querySuperFiles",
+        columns: {
+            col1: selector({ selectorType: "checkbox", width: 25 }),
+            File: {
+                label: nlsHPCC.File,
+                formatter: function (item, row) {
+                    return `<a href="#/files/${querySet}/${item}">${item}</a>`;
+                }
+            },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -94,7 +89,7 @@ export const QuerySuperFiles: React.FunctionComponent<QuerySuperFilesProps> = ({
             .then(({ WUQueryDetailsResponse }) => {
                 const superFiles = query?.SuperFiles?.SuperFile;
                 if (superFiles) {
-                    gridStore.setData(superFiles.map((item, idx) => {
+                    store.setData(superFiles.map((item, idx) => {
                         return {
                             __hpcc_id: idx,
                             File: item.Name
@@ -104,10 +99,10 @@ export const QuerySuperFiles: React.FunctionComponent<QuerySuperFilesProps> = ({
                 }
             })
             .catch(logger.error);
-    }, [gridStore, query, refreshTable]);
+    }, [store, query, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} />}
-        main={<DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
+        main={<Grid />}
     />;
 };

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

@@ -4,10 +4,11 @@ import { useConst } from "@fluentui/react-hooks";
 import { AlphaNumSortMemory } from "src/Memory";
 import * as Observable from "dojo/store/Observable";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitResources } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 const defaultUIState = {
     hasSelection: false
@@ -21,35 +22,29 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     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 store = useConst(new Observable(new AlphaNumSortMemory("DisplayPath", { Name: true, Value: true })));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "resources",
+        columns: {
+            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 = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -83,10 +78,6 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
         },
     ], [refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "roxiequeries.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -99,19 +90,19 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        gridStore.setData(resources.filter((row, idx) => idx > 0).map(row => {
+        store.setData(resources.filter((row, idx) => idx > 0).map(row => {
             return {
                 URL: row,
                 DisplayPath: row.substring(`res/${wuid}/`.length)
             };
         }));
         refreshTable();
-    }, [gridStore, refreshTable, resources, wuid]);
+    }, [store, refreshTable, resources, wuid]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

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

@@ -4,10 +4,11 @@ import { useConst } from "@fluentui/react-hooks";
 import { AlphaNumSortMemory } from "src/Memory";
 import * as Observable from "dojo/store/Observable";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitResults } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 const defaultUIState = {
     hasSelection: false
@@ -21,56 +22,50 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [results] = useWorkunitResults(wuid);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Name: {
-            label: nlsHPCC.Name, width: 180, sortable: true,
-            formatter: function (Name, row) {
-                return `<a href='#/workunits/${row.Wuid}/outputs/${Name}' class='dgrid-row-url'>${Name}</a>`;
-            }
-        },
-        FileName: {
-            label: nlsHPCC.FileName, sortable: true,
-            formatter: function (FileName, idx) {
-                return `<a href='#/files/${FileName}' class='dgrid-row-url2'>${FileName}</a>`;
-            }
-        },
-        Value: {
-            label: nlsHPCC.Value,
-            width: 180,
-            sortable: true
-        },
-        ResultViews: {
-            label: nlsHPCC.Views, sortable: true,
-            formatter: function (ResultViews, idx) {
-                let retVal = "";
-                ResultViews?.forEach((item, idx) => {
-                    retVal += "<a href='#' onClick='return false;' viewName=" + encodeURIComponent(item) + " class='dgrid-row-url3'>" + item + "</a>&nbsp;";
-                });
-                return retVal;
+    const store = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "results",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Name: {
+                label: nlsHPCC.Name, width: 180, sortable: true,
+                formatter: function (Name, row) {
+                    return `<a href='#/workunits/${row.Wuid}/outputs/${Name}' class='dgrid-row-url'>${Name}</a>`;
+                }
+            },
+            FileName: {
+                label: nlsHPCC.FileName, sortable: true,
+                formatter: function (FileName, idx) {
+                    return `<a href='#/files/${FileName}' class='dgrid-row-url2'>${FileName}</a>`;
+                }
+            },
+            Value: {
+                label: nlsHPCC.Value,
+                width: 180,
+                sortable: true
+            },
+            ResultViews: {
+                label: nlsHPCC.Views, sortable: true,
+                formatter: function (ResultViews, idx) {
+                    let retVal = "";
+                    ResultViews?.forEach((item, idx) => {
+                        retVal += "<a href='#' onClick='return false;' viewName=" + encodeURIComponent(item) + " class='dgrid-row-url3'>" + item + "</a>&nbsp;";
+                    });
+                    return retVal;
+                }
             }
         }
     });
 
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [grid, gridQuery]);
-
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -104,10 +99,6 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
         },
     ], [refreshTable, selection, uiState.hasSelection, wuid]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "results.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -120,7 +111,7 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        gridStore.setData(results.map(row => {
+        store.setData(results.map(row => {
             const tmp: any = row.ResultViews;
             return {
                 __hpcc_id: row.Name,
@@ -133,12 +124,12 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
             };
         }));
         refreshTable();
-    }, [gridStore, refreshTable, results]);
+    }, [store, refreshTable, results]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 30 - 37
esp/src/src-react/components/Search.tsx

@@ -3,9 +3,10 @@ import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Pivot, PivotI
 import { useConst } from "@fluentui/react-hooks";
 import { ESPSearch } from "src/ESPSearch";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 import { Workunits } from "./Workunits";
 import { Files } from "./Files";
 import { Queries } from "./Queries";
@@ -27,37 +28,10 @@ export const Search: React.FunctionComponent<SearchProps> = ({
 
     const progress = useConst({ value: 0 });
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [mine, setMine] = React.useState(false);
-    const [selection, setSelection] = React.useState([]);
     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 => {
@@ -70,6 +44,28 @@ export const Search: React.FunctionComponent<SearchProps> = ({
             setSearchCount(0);
         }));
 
+    //  Grid ---
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: search.store,
+        filename: "search",
+        columns: {
+            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>";
+                }
+            }
+        }
+    });
+
     React.useEffect(() => {
         if (searchText) {
             progress.value = 0;
@@ -107,10 +103,6 @@ export const Search: React.FunctionComponent<SearchProps> = ({
         },
     ], [mine, refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "search.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -124,19 +116,20 @@ export const Search: React.FunctionComponent<SearchProps> = ({
     const [selectedKey, setSelectedKey] = React.useState("all");
 
     return <HolyGrail
-        header={<Pivot headersOnly={true} onLinkClick={(item: PivotItem) => setSelectedKey(item.props.itemKey!)}>
+        header={<Pivot headersOnly={true} onLinkClick={(item: PivotItem) => setSelectedKey(item.props.itemKey!)
+        }>
             <PivotItem itemKey="all" headerText={nlsHPCC.All} itemCount={search.store.data.length} />
             <PivotItem itemKey="ecl" headerText={nlsHPCC.ECLWorkunit} headerButtonProps={search.eclStore.data.length === 0 ? disabled : undefined} itemCount={search.eclStore.data.length} />
             <PivotItem itemKey="dfu" headerText={nlsHPCC.DFUWorkunit} headerButtonProps={search.dfuStore.data.length === 0 ? disabled : undefined} itemCount={search.dfuStore.data.length} />
             <PivotItem itemKey="file" headerText={nlsHPCC.LogicalFile} headerButtonProps={search.fileStore.data.length === 0 ? disabled : undefined} itemCount={search.fileStore.data.length} />
             <PivotItem itemKey="query" headerText={nlsHPCC.Query} headerButtonProps={search.queryStore.data.length === 0 ? disabled : undefined} itemCount={search.queryStore.data.length} />
-        </Pivot>}
+        </Pivot >}
         main={selectedKey === "all" ? <HolyGrail
             header={<>
-                <CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />
+                <CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />
                 <ProgressIndicator progressHidden={searchCount === 0} percentComplete={searchCount === 0 ? 0 : progress.value / searchCount} />
             </>}
-            main={<DojoGrid store={search.store} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />}
+            main={<Grid />}
         /> : selectedKey === "ecl" ?
             <Workunits store={search.eclStore} /> : selectedKey === "dfu" ?
                 <DFUWorkunits store={search.dfuStore} /> : selectedKey === "file" ?

+ 31 - 39
esp/src/src-react/components/SourceFiles.tsx

@@ -6,10 +6,11 @@ import * as domClass from "dojo/dom-class";
 import { AlphaNumSortMemory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitSourceFiles } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector, tree } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector, tree } from "./DojoGrid";
 
 const defaultUIState = {
     hasSelection: false
@@ -34,43 +35,38 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [variables] = useWorkunitSourceFiles(wuid);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new TreeStore("Name", { Name: true, Value: true })));
-    const gridSort = useConst([{ attribute: "Name", "descending": false }]);
-    const gridQuery = useConst({ __hpcc_parentName: "" });
-    const gridColumns = useConst({
-        col1: selector({
-            width: 27,
-            selectorType: "checkbox"
-        }),
-        Name: tree({
-            label: "Name", sortable: true,
-            formatter: function (Name, row) {
-                return Utility.getImageHTML(row.IsSuperFile ? "folder_table.png" : "file.png") + "&nbsp;<a href='#' onClick='return false;' class='dgrid-row-url'>" + Name + "</a>";
+    const store = useConst(new Observable(new TreeStore("Name", { Name: true, Value: true })));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Name", "descending": false }],
+        query: { __hpcc_parentName: "" },
+        filename: "sourceFiles",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Name: tree({
+                label: "Name", sortable: true,
+                formatter: function (Name, row) {
+                    return Utility.getImageHTML(row.IsSuperFile ? "folder_table.png" : "file.png") + "&nbsp;<a href='#' onClick='return false;' class='dgrid-row-url'>" + Name + "</a>";
+                }
+            }),
+            FileCluster: { label: nlsHPCC.FileCluster, width: 300, sortable: false },
+            Count: {
+                label: nlsHPCC.Usage, width: 72, sortable: true,
+                renderCell: function (object, value, node, options) {
+                    domClass.add(node, "justify-right");
+                    node.innerText = Utility.valueCleanUp(value);
+                },
             }
-        }),
-        FileCluster: { label: nlsHPCC.FileCluster, width: 300, sortable: false },
-        Count: {
-            label: nlsHPCC.Usage, width: 72, sortable: true,
-            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 = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -92,10 +88,6 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
         },
     ], [refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "sourcefiles.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -108,14 +100,14 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        gridStore.setData(variables);
+        store.setData(variables);
         refreshTable();
-    }, [gridStore, refreshTable, variables]);
+    }, [store, refreshTable, variables]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 19 - 28
esp/src/src-react/components/SuperFiles.tsx

@@ -4,10 +4,11 @@ import { useConst } from "@fluentui/react-hooks";
 import * as Observable from "dojo/store/Observable";
 import { Memory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 
 const defaultUIState = {
     hasSelection: false
@@ -24,28 +25,22 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
 }) => {
 
     const [file, , _refresh] = useFile(cluster, logicalFile);
-    const [grid, setGrid] = React.useState<any>(undefined);
-    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();
+    const store = useConst(new Observable(new Memory("Name")));
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Name", "descending": false }],
+        filename: "superFiles",
+        columns: {
+            col1: selector({
+                width: 27,
+                selectorType: "checkbox"
+            }),
+            Name: { label: nlsHPCC.Name, sortable: true, },
         }
-    }, [grid, gridQuery]);
+    });
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -68,10 +63,6 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
         },
     ], [cluster, refreshTable, selection, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "syper_files.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -84,15 +75,15 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
 
     React.useEffect(() => {
         if (file?.Superfiles?.DFULogicalFile) {
-            gridStore?.setData(file?.Superfiles?.DFULogicalFile);
+            store?.setData(file?.Superfiles?.DFULogicalFile);
             refreshTable();
         }
-    }, [file, gridStore, refreshTable]);
+    }, [file, store, refreshTable]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

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

@@ -1,11 +1,11 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 import { scopedLogger } from "@hpcc-js/util";
 import * as WsAccess from "src/ws_access";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 import { AddUserForm } from "./forms/AddUser";
 import { Filter } from "./forms/Filter";
 import { Fields } from "./forms/Fields";
@@ -42,29 +42,30 @@ export const Users: React.FunctionComponent<UsersProps> = ({
     filter = emptyFilter
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [showAddUser, setShowAddUser] = React.useState(false);
     const [showFilter, setShowFilter] = React.useState(false);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
 
     //  Grid ---
-    const gridStore = useConst(WsAccess.CreateUsersStore(null, true));
-    const gridSort = useConst([{ attribute: "username", "descending": false }]);
-    const gridQuery = useConst(formatQuery(filter));
-    const gridColumns = useConst({
-        check: selector({ width: 27 }, "checkbox"),
-        username: {
-            width: 180,
-            label: nlsHPCC.Username,
-            formatter: function (_name, idx) {
-                return `<a href="#/security/users/${_name}">${_name}</a>`;
-            }
-        },
-        employeeID: { width: 180, label: nlsHPCC.EmployeeID },
-        employeeNumber: { width: 180, label: nlsHPCC.EmployeeNumber },
-        fullname: { label: nlsHPCC.FullName },
-        passwordexpiration: { width: 180, label: nlsHPCC.PasswordExpiration }
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: WsAccess.CreateUsersStore(null, true),
+        query: formatQuery(filter),
+        sort: [{ attribute: "username", "descending": false }],
+        filename: "users",
+        columns: {
+            check: selector({ width: 27 }, "checkbox"),
+            username: {
+                width: 180,
+                label: nlsHPCC.Username,
+                formatter: function (_name, idx) {
+                    return `<a href="#/security/users/${_name}">${_name}</a>`;
+                }
+            },
+            employeeID: { width: 180, label: nlsHPCC.EmployeeID },
+            employeeNumber: { width: 180, label: nlsHPCC.EmployeeNumber },
+            fullname: { label: nlsHPCC.FullName },
+            passwordexpiration: { width: 180, label: nlsHPCC.PasswordExpiration }
+        }
     });
 
     //  Selection  ---
@@ -78,13 +79,6 @@ export const Users: React.FunctionComponent<UsersProps> = ({
         setUIState(state);
     }, [selection]);
 
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", formatQuery(filter));
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [filter, grid]);
-
     const exportUsers = React.useCallback(() => {
         let usernames = "";
         selection.forEach((item, idx) => {
@@ -157,16 +151,12 @@ export const Users: React.FunctionComponent<UsersProps> = ({
         filterFields[field] = { ...FilterFields[field], value: filter[field] };
     }
 
-    React.useEffect(() => {
-        refreshTable();
-    }, [filter, grid?.data, refreshTable]);
-
     return <>
         <HolyGrail
-            header={<CommandBar items={buttons} overflowButtonProps={{}} />}
+            header={<CommandBar items={buttons} farItems={copyButtons} />}
             main={
                 <>
-                    <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                    <Grid />
                     <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
                 </>
             }

+ 16 - 25
esp/src/src-react/components/Variables.tsx

@@ -4,10 +4,10 @@ import { useConst } from "@fluentui/react-hooks";
 import * as Observable from "dojo/store/Observable";
 import { AlphaNumSortMemory } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitVariables } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
 
 interface VariablesProps {
     wuid: string;
@@ -17,35 +17,30 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [variables] = useWorkunitVariables(wuid);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        Type: { label: nlsHPCC.Type, width: 180 },
-        Name: { label: nlsHPCC.Name, width: 360 },
-        Value: { label: nlsHPCC.Value }
-    });
-
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", {});
-        if (clearSelection) {
-            grid?.clearSelection();
+    const store = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "variables",
+        columns: {
+            Type: { label: nlsHPCC.Type, width: 180 },
+            Name: { label: nlsHPCC.Name, width: 360 },
+            Value: { label: nlsHPCC.Value }
         }
-    }, [grid]);
+    });
 
     React.useEffect(() => {
-        gridStore.setData(variables.map((row, idx) => {
+        store.setData(variables.map((row, idx) => {
             return {
                 __hpcc_id: idx,
                 ...row
             };
         }));
         refreshTable();
-    }, [gridStore, refreshTable, variables]);
+    }, [store, refreshTable, variables]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -56,14 +51,10 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
         { 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} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
-            <DojoGrid store={gridStore} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 29 - 39
esp/src/src-react/components/Workflows.tsx

@@ -4,10 +4,10 @@ import { useConst } from "@fluentui/react-hooks";
 import { AlphaNumSortMemory } from "src/Memory";
 import * as Observable from "dojo/store/Observable";
 import nlsHPCC from "src/nlsHPCC";
+import { useGrid } from "../hooks/grid";
 import { useWorkunitWorkflows } from "../hooks/workunit";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
 
 interface WorkflowsProps {
     wuid: string;
@@ -17,53 +17,47 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
     wuid
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
-    const [selection, setSelection] = React.useState([]);
     const [workflows, , refreshWorkflow] = useWorkunitWorkflows(wuid);
 
     //  Grid ---
-    const gridStore = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
-    const gridQuery = useConst({});
-    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
-    const gridColumns = useConst({
-        EventName: { label: nlsHPCC.Name, width: 180 },
-        EventText: { label: nlsHPCC.Subtype },
-        Count: {
-            label: nlsHPCC.Count, width: 180,
-            formatter: function (count) {
-                if (count === -1) {
-                    return 0;
+    const store = useConst(new Observable(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true })));
+    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
+        store,
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "workflows",
+        columns: {
+            EventName: { label: nlsHPCC.Name, width: 180 },
+            EventText: { label: nlsHPCC.Subtype },
+            Count: {
+                label: nlsHPCC.Count, width: 180,
+                formatter: function (count) {
+                    if (count === -1) {
+                        return 0;
+                    }
+                    return count;
                 }
-                return count;
-            }
-        },
-        CountRemaining: {
-            label: nlsHPCC.Remaining, width: 180,
-            formatter: function (countRemaining) {
-                if (countRemaining === -1) {
-                    return 0;
+            },
+            CountRemaining: {
+                label: nlsHPCC.Remaining, width: 180,
+                formatter: function (countRemaining) {
+                    if (countRemaining === -1) {
+                        return 0;
+                    }
+                    return countRemaining;
                 }
-                return countRemaining;
             }
         }
     });
 
-    const refreshTable = React.useCallback((clearSelection = false) => {
-        grid?.set("query", gridQuery);
-        if (clearSelection) {
-            grid?.clearSelection();
-        }
-    }, [grid, gridQuery]);
-
     React.useEffect(() => {
-        gridStore.setData(workflows.map(row => {
+        store.setData(workflows.map(row => {
             return {
                 ...row,
                 __hpcc_id: row.WFID
             };
         }));
         refreshTable();
-    }, [gridStore, refreshTable, workflows]);
+    }, [store, refreshTable, workflows]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -76,14 +70,10 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
         { 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} />}
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={copyButtons} />}
         main={
-            <DojoGrid type="SimpleGrid" store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+            <Grid />
         }
     />;
 };

+ 45 - 59
esp/src/src-react/components/Workunits.tsx

@@ -1,20 +1,20 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 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 { useGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { pushParams } from "../util/history";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
-import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
-import { DojoGrid, selector } from "./DojoGrid";
+import { ShortVerticalDivider } from "./Common";
+import { selector } from "./DojoGrid";
 import { scopedLogger } from "@hpcc-js/util";
 
-const logger = scopedLogger("../components/Workunits.tsx");
+const logger = scopedLogger("src-react/components/Workunits.tsx");
 
 const FilterFields: Fields = {
     "Type": { type: "checkbox", label: nlsHPCC.ArchivedOnly },
@@ -74,72 +74,62 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
     store
 }) => {
 
-    const [grid, setGrid] = React.useState<any>(undefined);
     const [showFilter, setShowFilter] = React.useState(false);
     const [mine, setMine] = React.useState(false);
-    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);
+    const [Grid, selection, refreshTable, copyButtons] = useGrid({
+        store: store ? store : ESPWorkunit.CreateWUQueryStore({}),
+        query: formatQuery(filter),
+        sort: [{ attribute: "Wuid", "descending": true }],
+        filename: "workunits",
+        columns: {
+            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 "";
+                }
             },
-            width: 25,
-            sortable: false,
-            formatter: function (_protected) {
-                if (_protected === true) {
-                    return Utility.getImageHTML("locked.png");
+            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;
                 }
-                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 = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -201,10 +191,6 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
         },
     ], [mine, refreshTable, selection, store, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
 
-    const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
-        ...createCopyDownloadSelection(grid, selection, "workunits.csv")
-    ], [grid, selection]);
-
     //  Selection  ---
     React.useEffect(() => {
         const state = { ...defaultUIState };
@@ -235,10 +221,10 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
     }, [selection]);
 
     return <HolyGrail
-        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        header={<CommandBar items={buttons} farItems={copyButtons} />}
         main={
             <>
-                <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+                <Grid />
                 <Filter showFilter={showFilter} setShowFilter={setShowFilter} filterFields={filterFields} onApply={pushParams} />
             </>
         }

+ 38 - 0
esp/src/src-react/hooks/deepHooks.ts

@@ -0,0 +1,38 @@
+import * as React from "react";
+import { deepEquals } from "@hpcc-js/util";
+
+//  Inpired from:  https://github.com/kentcdodds/use-deep-compare-effect
+
+function useDeepCompareMemoize<T>(value: T) {
+    const ref = React.useRef<T>(value);
+    const signalRef = React.useRef<number>(0);
+
+    if (!deepEquals(value, ref.current)) {
+        ref.current = value;
+        signalRef.current += 1;
+    }
+
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    return React.useMemo(() => ref.current, [signalRef.current]);
+}
+
+type UseEffectParams = Parameters<typeof React.useEffect>
+type EffectCallback = UseEffectParams[0]
+type DependencyList = UseEffectParams[1]
+type UseEffectReturn = ReturnType<typeof React.useEffect>
+
+export function useDeepEffect(callback: EffectCallback, dependencies: DependencyList, deepDependencies: DependencyList): UseEffectReturn {
+
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    return React.useEffect(callback, [...dependencies, ...useDeepCompareMemoize(deepDependencies)]);
+}
+
+type UseCallbackParams = Parameters<typeof React.useCallback>
+type CallbackCallback = UseCallbackParams[0]
+type UseCallbackReturn = ReturnType<typeof React.useCallback>
+
+export function useDeepCallback(callback: CallbackCallback, dependencies: DependencyList, deepDependencies: DependencyList): UseCallbackReturn {
+
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    return React.useCallback(callback, [...dependencies, ...useDeepCompareMemoize(deepDependencies)]);
+}

+ 0 - 10
esp/src/src-react/hooks/grid.ts

@@ -1,10 +0,0 @@
-
-export function useGrid(store, filter, sort, columns) {
-
-    return {
-        store,
-        filter,
-        sort,
-        columns
-    };
-} 

+ 53 - 0
esp/src/src-react/hooks/grid.tsx

@@ -0,0 +1,53 @@
+import * as React from "react";
+import { ICommandBarItemProps } from "@fluentui/react";
+import { useConst } from "@fluentui/react-hooks";
+import { createCopyDownloadSelection } from "../components/Common";
+import { DojoGrid } from "../components/DojoGrid";
+import { useDeepCallback, useDeepEffect } from "./deepHooks";
+
+interface useGridProps {
+    store: any,
+    query?: object,
+    sort?: object[],
+    columns: object,
+    getSelected?: () => any[],
+    filename: string
+}
+export function useGrid({ store, query = {}, sort = [], columns, getSelected, filename }: useGridProps): [React.FunctionComponent, any[], (clearSelection?: boolean) => void, ICommandBarItemProps[]] {
+
+    const constStore = useConst(store);
+    const constQuery = useConst({ ...query });
+    const constSort = useConst([...sort]);
+    const constColumns = useConst({ ...columns });
+    const constGetSelected = useConst(() => getSelected);
+    const [grid, setGrid] = React.useState<any>(undefined);
+    const [selection, setSelection] = React.useState([]);
+
+    const Grid = React.useMemo(() => () => <DojoGrid
+        store={constStore}
+        query={constQuery}
+        sort={constSort}
+        columns={constColumns}
+        getSelected={constGetSelected}
+
+        setGrid={setGrid}
+        setSelection={setSelection} />,
+        [constColumns, constGetSelected, constQuery, constSort, constStore]);
+
+    const refreshTable = useDeepCallback((clearSelection = false) => {
+        grid?.set("query", { ...query });
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [grid], [query]);
+
+    useDeepEffect(() => {
+        refreshTable();
+    }, [refreshTable], [query]);
+
+    const copyButtons = React.useMemo((): ICommandBarItemProps[] => [
+        ...createCopyDownloadSelection(grid, selection, `${filename}.csv`)
+    ], [filename, grid, selection]);
+
+    return [Grid, selection, refreshTable, copyButtons];
+}