Browse Source

Merge pull request #15599 from GordonSmith/HPCC-26828-FluentGridLF

HPCC-26828 Port LogicalFile tables to React

Reviewed-By: Jeremy Clements <jeremy.clements@lexisnexisrisk.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 3 years ago
parent
commit
a7094c012a

+ 10 - 13
esp/src/src-react/components/FileBlooms.tsx

@@ -1,10 +1,7 @@
 import * as React from "react";
 import { CommandBar, ICommandBarItemProps } from "@fluentui/react";
-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 { useFluentGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
 
@@ -19,32 +16,32 @@ export const FileBlooms: React.FunctionComponent<FileBloomsProps> = ({
 }) => {
 
     const [file, , , refreshData] = useFile(cluster, logicalFile);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("FieldNames")));
-    const [Grid, , refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, , copyButtons] = useFluentGrid({
+        data,
+        primaryID: "FieldNames",
         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, }
+            FieldNames: { label: nlsHPCC.FieldNames, sortable: true, width: 320 },
+            Limit: { label: nlsHPCC.Limit, sortable: true, width: 180 },
+            Probability: { label: nlsHPCC.Probability, sortable: true, width: 180 },
         }
     });
 
     React.useEffect(() => {
         const fileBlooms = file?.Blooms?.DFUFileBloom;
         if (fileBlooms) {
-            store.setData(fileBlooms.map(bloom => {
+            setData(fileBlooms.map(bloom => {
                 return {
                     ...bloom,
                     FieldNames: bloom?.FieldNames?.Item[0] || "",
                 };
             }));
-            refreshTable();
         }
-    }, [file?.Blooms?.DFUFileBloom, refreshTable, store]);
+    }, [file?.Blooms?.DFUFileBloom]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [

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

@@ -54,19 +54,19 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({
                     : <></>
                 }
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.Contents} itemKey="Contents" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.Contents} itemKey="contents" style={pivotItemStyle(size, 0)}>
                 <Result cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.DataPatterns} itemKey="DataPatterns" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.DataPatterns} itemKey="datapatterns" style={pivotItemStyle(size, 0)}>
                 <DataPatterns cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.ECL} itemKey="ECL" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.ECL} itemKey="ecl" style={pivotItemStyle(size, 0)}>
                 <ECLSourceEditor text={file?.Ecl} readonly={true} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.DEF} itemKey="DEF" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.DEF} itemKey="def" style={pivotItemStyle(size, 0)}>
                 <XMLSourceEditor text={defFile} readonly={true} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.XML} itemKey="XML" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.XML} itemKey="xml" style={pivotItemStyle(size, 0)}>
                 <XMLSourceEditor text={xmlFile} readonly={true} />
             </PivotItem>
             {file?.isSuperfile
@@ -77,22 +77,22 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({
                     <SuperFiles cluster={cluster} logicalFile={logicalFile} />
                 </PivotItem>
             }
-            <PivotItem headerText={nlsHPCC.FileParts} itemKey="FileParts" itemCount={file?.fileParts().length ?? 0} style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.FileParts} itemKey="parts" itemCount={file?.fileParts().length ?? 0} style={pivotItemStyle(size, 0)}>
                 <FileParts cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
             <PivotItem headerText={nlsHPCC.Queries} itemKey="queries" style={pivotItemStyle(size, 0)}>
                 <Queries filter={{ FileName: logicalFile }} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.Graphs} itemKey="Graphs" itemCount={file?.Graphs?.ECLGraph?.length} headerButtonProps={{ disabled: isDFUWorkunit }} style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.Graphs} itemKey="graphs" itemCount={file?.Graphs?.ECLGraph?.length} headerButtonProps={{ disabled: isDFUWorkunit }} style={pivotItemStyle(size, 0)}>
                 <FileDetailsGraph cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.History} itemKey="History" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.History} itemKey="history" style={pivotItemStyle(size, 0)}>
                 <FileHistory cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.Blooms} itemKey="Blooms" itemCount={file?.Blooms?.DFUFileBloom?.length} style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.Blooms} itemKey="blooms" itemCount={file?.Blooms?.DFUFileBloom?.length} style={pivotItemStyle(size, 0)}>
                 <FileBlooms cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.ProtectBy} itemKey="ProtectBy" style={pivotItemStyle(size, 0)}>
+            <PivotItem headerText={nlsHPCC.ProtectBy} itemKey="protectby" style={pivotItemStyle(size, 0)}>
                 <ProtectedBy cluster={cluster} logicalFile={logicalFile} />
             </PivotItem>
         </Pivot>

+ 17 - 14
esp/src/src-react/components/FileDetailsGraph.tsx

@@ -1,10 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { Memory, Observable } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Image, Link } from "@fluentui/react";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
-import { useGrid } from "../hooks/grid";
+import { useFluentGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
@@ -37,11 +35,12 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
 
     const [file, , , refreshData] = useFile(cluster, logicalFile);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("Name")));
-    const [Grid, selection, refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "Name",
         filename: "graphs",
         columns: {
             col1: selector({
@@ -51,7 +50,11 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
             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>`;
+                    return <>
+                        <Image src={Utility.getImageURL(getStateImageName(row))} />
+                        &nbsp;
+                        <Link href={`#/workunits/${row?.Wuid}/metrics/${Name}`}>{Name}</Link>
+                    </>;
                 }
             }
         }
@@ -68,10 +71,10 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
             key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
             onClick: () => {
                 if (selection.length === 1) {
-                    window.location.href = `#/workunits/${file?.Wuid}/graphs/${selection[0].Name}`;
+                    window.location.href = `#/workunits/${file?.Wuid}/metrics/${selection[0].Name}`;
                 } else {
                     for (let i = 0; i < selection.length; ++i) {
-                        window.open(`#/workunits/${file?.Wuid}/graphs/${selection[i].Name}`, "_blank");
+                        window.open(`#/workunits/${file?.Wuid}/metrics/${selection[i].Name}`, "_blank");
                     }
                 }
             }
@@ -86,17 +89,17 @@ export const FileDetailsGraph: React.FunctionComponent<FileDetailsGraphProps> =
     }, [selection]);
 
     React.useEffect(() => {
-        store.setData((file?.Graphs?.ECLGraph || []).map(item => {
+        setData((file?.Graphs?.ECLGraph || []).map(item => {
             return {
                 Name: item,
                 Label: "",
                 Completed: "",
                 Time: 0,
-                Type: ""
+                Type: "",
+                Wuid: file?.Wuid
             };
         }));
-        refreshTable();
-    }, [store, file?.Graphs?.ECLGraph, refreshTable]);
+    }, [file?.Graphs?.ECLGraph, file?.Wuid]);
 
     return <HolyGrail
         header={<CommandBar items={buttons} farItems={copyButtons} />}

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

@@ -1,11 +1,9 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { Memory, Observable } from "src/Memory";
 import nlsHPCC from "src/nlsHPCC";
 import { useConfirm } from "../hooks/confirm";
 import { useFileHistory } from "../hooks/file";
-import { useGrid } from "../hooks/grid";
+import { useFluentGrid } from "../hooks/grid";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 
@@ -21,10 +19,11 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
 
     //  Grid ---
     const [history, eraseHistory, refreshData] = useFileHistory(cluster, logicalFile);
+    const [data, setData] = React.useState<any[]>([]);
 
-    const store = useConst(new Observable(new Memory("Name")));
-    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, _selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "__hpcc_id",
         sort: [{ attribute: "Name", "descending": false }],
         filename: "filehistory",
         columns: {
@@ -45,9 +44,8 @@ export const FileHistory: React.FunctionComponent<FileHistoryProps> = ({
     });
 
     React.useEffect(() => {
-        store.setData(history);
-        refreshTable();
-    }, [history, refreshTable, store]);
+        setData(history.map((item, idx) => ({ ...item, __hpcc_id: idx })));
+    }, [history]);
 
     //  Command Bar  ---
     const buttons: ICommandBarItemProps[] = React.useMemo(() => [

+ 13 - 16
esp/src/src-react/components/FileParts.tsx

@@ -1,11 +1,8 @@
 import * as React from "react";
 import { ICommandBarItemProps, CommandBar } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
 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 { useFluentGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
 
@@ -22,26 +19,27 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
 }) => {
 
     const [file, , , refreshData] = useFile(cluster, logicalFile);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("Id")));
-    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, _selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "Id",
         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, },
+            Id: { label: nlsHPCC.Part, sortable: true, width: 80 },
+            Copy: { label: nlsHPCC.Copy, sortable: true, width: 80 },
+            Ip: { label: nlsHPCC.IP, sortable: true, width: 80 },
+            Cluster: { label: nlsHPCC.Cluster, sortable: true, width: 480 },
+            PartsizeInt64: { label: nlsHPCC.Size, sortable: true, width: 120 },
+            CompressedSize: { label: nlsHPCC.CompressedSize, sortable: true, width: 120 },
         }
     });
 
     React.useEffect(() => {
         const fileParts = file?.fileParts() ?? [];
-        store.setData(fileParts.map(part => {
+        setData(fileParts.map(part => {
             return {
                 Id: part.Id,
                 Copy: part.Copy,
@@ -51,8 +49,7 @@ export const FileParts: React.FunctionComponent<FilePartsProps> = ({
                 CompressedSize: part.CompressedSize ? formatNum(part.CompressedSize) : ""
             };
         }));
-        refreshTable();
-    }, [cluster, file, refreshTable, store]);
+    }, [cluster, file]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [

+ 12 - 14
esp/src/src-react/components/Helpers.tsx

@@ -1,14 +1,11 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import * as domClass from "dojo/dom-class";
 import * as ESPRequest from "src/ESPRequest";
-import { Memory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { HelperRow, useWorkunitHelpers } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
 
@@ -102,11 +99,12 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
 
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [helpers, refreshData] = useWorkunitHelpers(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Memory("id"));
     const [Grid, selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "id",
         filename: "helpers",
         columns: {
             sel: selector({
@@ -205,13 +203,13 @@ export const Helpers: React.FunctionComponent<HelpersProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        store.setData(helpers);
-    }, [store, helpers]);
+        setData(helpers);
+    }, [helpers]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

+ 9 - 12
esp/src/src-react/components/ProtectedBy.tsx

@@ -1,10 +1,7 @@
 import * as React from "react";
 import { CommandBar, ICommandBarItemProps } from "@fluentui/react";
-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 { useFluentGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
 import { HolyGrail } from "../layouts/HolyGrail";
 
@@ -19,16 +16,17 @@ export const ProtectedBy: React.FunctionComponent<ProtectedByProps> = ({
 }) => {
 
     const [file, , , refreshData] = useFile(cluster, logicalFile);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("Owner")));
-    const [Grid, _selection, refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, _selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "Owner",
         sort: [{ attribute: "Owner", "descending": false }],
         filename: "protectedBy",
         columns: {
-            Owner: { label: nlsHPCC.Owner, sortable: false },
-            Modified: { label: nlsHPCC.Modified, sortable: false },
+            Owner: { label: nlsHPCC.Owner, width: 320 },
+            Modified: { label: nlsHPCC.Modified, width: 320 },
         }
     });
 
@@ -36,15 +34,14 @@ export const ProtectedBy: React.FunctionComponent<ProtectedByProps> = ({
         const results = file?.ProtectList?.DFUFileProtect;
 
         if (results) {
-            store.setData(file?.ProtectList?.DFUFileProtect?.map(row => {
+            setData(file?.ProtectList?.DFUFileProtect?.map(row => {
                 return {
                     Owner: row.Owner,
                     Modified: row.Modified
                 };
             }));
-            refreshTable();
         }
-    }, [store, file?.ProtectList?.DFUFileProtect, refreshTable]);
+    }, [file?.ProtectList?.DFUFileProtect]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [

+ 13 - 14
esp/src/src-react/components/Resources.tsx

@@ -1,11 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { AlphaNumSortMemory } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { useWorkunitResources } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
 
@@ -23,11 +20,13 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
 
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [resources, , , refreshData] = useWorkunitResources(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new AlphaNumSortMemory("DisplayPath", { Name: true, Value: true }));
     const [Grid, selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "DisplayPath",
+        alphaNumColumns: { Name: true, Value: true },
         sort: [{ attribute: "Wuid", "descending": true }],
         filename: "resources",
         columns: {
@@ -89,18 +88,18 @@ export const Resources: React.FunctionComponent<ResourcesProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        store.setData(resources.filter((row, idx) => idx > 0).map(row => {
+        setData(resources.filter((row, idx) => idx > 0).map(row => {
             return {
                 URL: row,
                 DisplayPath: row.substring(`res/${wuid}/`.length)
             };
         }));
-    }, [store, resources, wuid]);
+    }, [resources, wuid]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

+ 13 - 14
esp/src/src-react/components/Results.tsx

@@ -1,11 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { AlphaNumSortMemory } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { useWorkunitResults } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
 
@@ -23,11 +20,13 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
 
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [results, , , refreshData] = useWorkunitResults(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true }));
     const [Grid, selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "__hpcc_id",
+        alphaNumColumns: { Name: true, Value: true },
         sort: [{ attribute: "Wuid", "descending": true }],
         filename: "results",
         columns: {
@@ -108,7 +107,7 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        store.setData(results.map(row => {
+        setData(results.map(row => {
             const tmp: any = row.ResultViews;
             return {
                 __hpcc_id: row.Name,
@@ -120,12 +119,12 @@ export const Results: React.FunctionComponent<ResultsProps> = ({
                 Sequence: row.Sequence
             };
         }));
-    }, [store, results]);
+    }, [results]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

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

@@ -1,15 +1,12 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Image, Link } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Image, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import * as domClass from "dojo/dom-class";
-import { AlphaNumSortMemory } from "src/Memory";
 import * as Utility from "src/Utility";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { useWorkunitSourceFiles } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
-import { selector, tree } from "./DojoGrid";
+import { selector } from "./DojoGrid";
 
 const defaultUIState = {
     hasSelection: false
@@ -19,28 +16,19 @@ interface SourceFilesProps {
     wuid: string;
 }
 
-class TreeStore extends AlphaNumSortMemory {
-
-    mayHaveChildren(item) {
-        return item.IsSuperFile;
-    }
-
-    getChildren(parent, options) {
-        return this.query({ __hpcc_parentName: parent.Name }, options);
-    }
-}
-
 export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     wuid
 }) => {
 
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
     const [sourceFiles, , , refreshData] = useWorkunitSourceFiles(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new TreeStore("Name", { Name: true, Value: true }));
     const [Grid, selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "Name",
+        alphaNumColumns: { Name: true, Value: true },
         sort: [{ attribute: "Name", "descending": false }],
         query: { __hpcc_parentName: "" },
         filename: "sourceFiles",
@@ -49,16 +37,16 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
                 width: 27,
                 selectorType: "checkbox"
             }),
-            Name: tree({
+            Name: {
                 label: "Name", sortable: true,
                 formatter: function (Name, row) {
                     return <>
-                        <Image src={Utility.getImageURL(row.IsSuperFile ? "folder_table.png" : "file.png")} className='iconAlign' />
+                        <Image src={Utility.getImageURL(row.IsSuperFile ? "folder_table.png" : "file.png")} />
                         &nbsp;
                         <Link href={`#/files/${row.FileCluster}/${Name}`}>{Name}</Link>
                     </>;
                 }
-            }),
+            },
             FileCluster: { label: nlsHPCC.FileCluster, width: 300, sortable: false },
             Count: {
                 label: nlsHPCC.Usage, width: 72, sortable: true,
@@ -103,13 +91,13 @@ export const SourceFiles: React.FunctionComponent<SourceFilesProps> = ({
     }, [selection]);
 
     React.useEffect(() => {
-        store.setData(sourceFiles);
-    }, [store, sourceFiles]);
+        setData(sourceFiles);
+    }, [sourceFiles]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

+ 32 - 40
esp/src/src-react/components/SubFiles.tsx

@@ -1,15 +1,11 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { HolyGrail } from "../layouts/HolyGrail";
+import { CommandBar, ContextualMenuItemType, FontIcon, ICommandBarItemProps, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import * as ESPLogicalFile from "src/ESPLogicalFile";
 import * as WsDfu from "src/WsDfu";
 import * as Utility from "src/Utility";
 import { useConfirm } from "../hooks/confirm";
 import { useFile } from "../hooks/file";
-import { useGrid } from "../hooks/grid";
-import * as Observable from "dojo/store/Observable";
-import { Memory } from "src/Memory";
+import { useFluentGrid } from "../hooks/grid";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
 import { pushUrl } from "../util/history";
@@ -31,11 +27,13 @@ export const SubFiles: React.FunctionComponent<SubFilesProps> = ({
 
     const [file, , , refresh] = useFile(cluster, logicalFile);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("Name")));
-    const [Grid, selection, refreshTable] = useGrid({
-        store: store,
+    const [Grid, selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "Name",
+        alphaNumColumns: { RecordCount: true, Totalsize: true },
         query: {},
         sort: [{ attribute: "Modified", "descending": true }],
         filename: "subfiles",
@@ -43,66 +41,60 @@ export const SubFiles: React.FunctionComponent<SubFilesProps> = ({
             sel: selector({ width: 27, selectorType: "checkbox" }),
             IsCompressed: {
                 width: 25, sortable: false,
-                renderHeaderCell: function (node) {
-                    node.innerHTML = Utility.getImageHTML("compressed.png", nlsHPCC.Compressed);
-                },
+                headerIcon: "ZipFolder",
+                headerTooltip: nlsHPCC.Compressed,
                 formatter: function (compressed) {
                     if (compressed === true) {
-                        return Utility.getImageHTML("compressed.png");
+                        return <FontIcon iconName="zipFolder" />;
                     }
-                    return "";
+                    return <></>;
                 }
             },
             IsKeyFile: {
                 width: 25, sortable: false,
-                renderHeaderCell: function (node) {
-                    node.innerHTML = Utility.getImageHTML("index.png", nlsHPCC.Index);
-                },
+                headerIcon: "Permissions",
+                headerTooltip: nlsHPCC.Index,
                 formatter: function (keyfile, row) {
                     if (row.ContentType === "key") {
-                        return Utility.getImageHTML("index.png");
+                        return <FontIcon iconName="Permissions" />;
                     }
-                    return "";
+                    return <></>;
                 }
             },
             isSuperfile: {
                 width: 25, sortable: false,
-                renderHeaderCell: function (node) {
-                    node.innerHTML = Utility.getImageHTML("superfile.png", nlsHPCC.Superfile);
-                },
+                headerIcon: "Folder",
+                headerTooltip: nlsHPCC.Superfile,
                 formatter: function (superfile) {
                     if (superfile === true) {
-                        return Utility.getImageHTML("superfile.png");
+                        return <FontIcon iconName="Folder" />;
                     }
-                    return "";
+                    return <></>;
                 }
             },
             Name: {
                 label: nlsHPCC.LogicalName,
                 formatter: function (name, row) {
                     const url = "#/files/" + (row.NodeGroup ? row.NodeGroup + "/" : "") + name;
-                    return "<a href='" + url + "'>" + name + "</a>";
+                    return <Link href={url}>{name}</Link>;
                 }
             },
             Owner: { label: nlsHPCC.Owner, width: 72 },
             Description: { label: nlsHPCC.Description, width: 153 },
             RecordCount: {
                 label: nlsHPCC.Records, width: 72, sortable: false,
-                renderCell: function (object, value, node, options) {
-                    node.innerText = Utility.valueCleanUp(value);
+                formatter: function (recordCount, row) {
+                    return Utility.valueCleanUp(recordCount);
                 },
             },
             Totalsize: {
                 label: nlsHPCC.Size, width: 72, sortable: false,
-                renderCell: function (object, value, node, options) {
-                    node.innerText = Utility.valueCleanUp(value);
+                formatter: function (totalSize, row) {
+                    return Utility.valueCleanUp(totalSize);
                 },
             },
             Parts: {
                 label: nlsHPCC.Parts, width: 45, sortable: false,
-                renderCell: function (object, value, node, options) {
-                    node.innerText = value;
-                },
             },
             Modified: { label: nlsHPCC.ModifiedUTCGMT, width: 155, sortable: false }
         }
@@ -130,10 +122,9 @@ export const SubFiles: React.FunctionComponent<SubFilesProps> = ({
             subfiles.push(logicalFile);
         });
         Promise.all(promises).then(logicalFiles => {
-            store.setData(subfiles);
-            refreshTable();
+            setData(subfiles);
         });
-    }, [store, file?.subfiles, refreshTable]);
+    }, [file?.subfiles]);
 
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
         {
@@ -166,11 +157,12 @@ export const SubFiles: React.FunctionComponent<SubFilesProps> = ({
     }, [selection]);
 
     return <>
-        <HolyGrail
-            header={<CommandBar items={buttons} />}
-            main={<Grid />}
-        />
+        <ScrollablePane>
+            <Sticky>
+                <CommandBar items={buttons} farItems={copyButtons} />
+            </Sticky>
+            <Grid />
+        </ScrollablePane>
         <DeleteSubfilesConfirm />
     </>;
-
 };

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

@@ -1,12 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import * as Observable from "dojo/store/Observable";
-import { Memory } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link, ScrollablePane, Sticky } from "@fluentui/react";
 import nlsHPCC from "src/nlsHPCC";
-import { useGrid } from "../hooks/grid";
+import { useFluentGrid } from "../hooks/grid";
 import { useFile } from "../hooks/file";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 import { selector } from "./DojoGrid";
 
@@ -26,11 +22,12 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
 
     const [file, , , refreshData] = useFile(cluster, logicalFile);
     const [uiState, setUIState] = React.useState({ ...defaultUIState });
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new Observable(new Memory("Name")));
-    const [Grid, selection, refreshTable, copyButtons] = useGrid({
-        store,
+    const [Grid, selection, copyButtons] = useFluentGrid({
+        data,
+        primaryID: "Name",
         sort: [{ attribute: "Name", "descending": false }],
         filename: "superFiles",
         columns: {
@@ -38,7 +35,13 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
                 width: 27,
                 selectorType: "checkbox"
             }),
-            Name: { label: nlsHPCC.Name, sortable: true, },
+            Name: {
+                label: nlsHPCC.Name,
+                sortable: true,
+                formatter: function (name, row) {
+                    return <Link href={`#/files/${cluster}/${name}`}>{name}</Link>;
+                }
+            },
         }
     });
 
@@ -75,15 +78,14 @@ export const SuperFiles: React.FunctionComponent<SuperFilesProps> = ({
 
     React.useEffect(() => {
         if (file?.Superfiles?.DFULogicalFile) {
-            store?.setData(file?.Superfiles?.DFULogicalFile);
-            refreshTable();
+            setData(file?.Superfiles?.DFULogicalFile);
         }
-    }, [file, store, refreshTable]);
+    }, [file]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

+ 13 - 14
esp/src/src-react/components/Variables.tsx

@@ -1,11 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { AlphaNumSortMemory } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, ScrollablePane, Sticky } from "@fluentui/react";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { useWorkunitVariables } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 
 interface VariablesProps {
@@ -17,11 +14,13 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
 }) => {
 
     const [variables, , , refreshData] = useWorkunitVariables(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true }));
     const [Grid, _selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "__hpcc_id",
+        alphaNumColumns: { Name: true, Value: true },
         sort: [{ attribute: "Wuid", "descending": true }],
         filename: "variables",
         columns: {
@@ -32,13 +31,13 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
     });
 
     React.useEffect(() => {
-        store.setData(variables.map((row, idx) => {
+        setData(variables.map((row, idx) => {
             return {
                 __hpcc_id: idx,
                 ...row
             };
         }));
-    }, [store, variables]);
+    }, [variables]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -49,10 +48,10 @@ export const Variables: React.FunctionComponent<VariablesProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
     ], [refreshData]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

+ 13 - 14
esp/src/src-react/components/Workflows.tsx

@@ -1,11 +1,8 @@
 import * as React from "react";
-import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
-import { useConst } from "@fluentui/react-hooks";
-import { AlphaNumSortMemory } from "src/Memory";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, ScrollablePane, Sticky } from "@fluentui/react";
 import nlsHPCC from "src/nlsHPCC";
 import { useFluentGrid } from "../hooks/grid";
 import { useWorkunitWorkflows } from "../hooks/workunit";
-import { HolyGrail } from "../layouts/HolyGrail";
 import { ShortVerticalDivider } from "./Common";
 
 interface WorkflowsProps {
@@ -17,11 +14,13 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
 }) => {
 
     const [workflows, , refreshWorkflow] = useWorkunitWorkflows(wuid);
+    const [data, setData] = React.useState<any[]>([]);
 
     //  Grid ---
-    const store = useConst(new AlphaNumSortMemory("__hpcc_id", { Name: true, Value: true }));
     const [Grid, _selection, copyButtons] = useFluentGrid({
-        store,
+        data,
+        primaryID: "__hpcc_id",
+        alphaNumColumns: { Name: true, Value: true },
         sort: [{ attribute: "Wuid", "descending": true }],
         filename: "workflows",
         columns: {
@@ -49,13 +48,13 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
     });
 
     React.useEffect(() => {
-        store.setData(workflows.map(row => {
+        setData(workflows.map(row => {
             return {
                 ...row,
                 __hpcc_id: row.WFID
             };
         }));
-    }, [store, workflows]);
+    }, [workflows]);
 
     //  Command Bar  ---
     const buttons = React.useMemo((): ICommandBarItemProps[] => [
@@ -68,10 +67,10 @@ export const Workflows: React.FunctionComponent<WorkflowsProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
     ], [refreshWorkflow]);
 
-    return <HolyGrail
-        header={<CommandBar items={buttons} farItems={copyButtons} />}
-        main={
-            <Grid />
-        }
-    />;
+    return <ScrollablePane>
+        <Sticky>
+            <CommandBar items={buttons} farItems={copyButtons} />
+        </Sticky>
+        <Grid />
+    </ScrollablePane>;
 };

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

@@ -1,6 +1,7 @@
 import * as React from "react";
-import { DetailsList, DetailsListLayoutMode, IColumn, ICommandBarItemProps, Selection } from "@fluentui/react";
+import { DetailsList, DetailsListLayoutMode, IColumn, ICommandBarItemProps, IDetailsHeaderProps, Selection, TooltipHost } from "@fluentui/react";
 import { useConst } from "@fluentui/react-hooks";
+import { AlphaNumSortMemory } from "src/Memory";
 import { createCopyDownloadSelection } from "../components/Common";
 import { DojoGrid } from "../components/DojoGrid";
 import { useDeepCallback, useDeepEffect } from "./deepHooks";
@@ -72,24 +73,58 @@ function columnsAdapter(columns, sorted: Sorted): IColumn[] {
                 isResizable: true,
                 isSorted: key == sorted.column,
                 isSortedDescending: key == sorted.column && sorted.descending,
+                iconName: column.headerIcon,
+                isIconOnly: !!column.headerIcon,
+                data: column
             } as IColumn);
         }
     }
     return retVal;
 }
 
-export function useFluentGrid({ store, query = {}, sort = [], columns, getSelected, filename }: useGridProps): [React.FunctionComponent, any[], ICommandBarItemProps[]] {
+export interface useFluentGrid2Props {
+    data: any[],
+    primaryID: string,
+    alphaNumColumns?: { [id: string]: boolean },
+    query?: object,
+    sort?: object[],
+    columns: object,
+    getSelected?: () => any[],
+    filename: string
+}
+
+export function useFluentGrid({ data, primaryID, alphaNumColumns = {}, query = {}, sort = [], columns, getSelected, filename }: useFluentGrid2Props): [React.FunctionComponent, any[], ICommandBarItemProps[]] {
 
+    const constStore = useConst(new AlphaNumSortMemory(primaryID, alphaNumColumns));
     const constQuery = useConst({ ...query });
     const constColumns = useConst({ ...columns });
     const [sorted, setSorted] = React.useState<Sorted>({ column: "", descending: false });
     const [selection, setSelection] = React.useState([]);
+    const [items, setItems] = React.useState<any[]>([]);
+
+    const refreshTable = React.useCallback(() => {
+        const sort = sorted.column ? [{ attribute: sorted.column, descending: sorted.descending }] : undefined;
+        constStore.query(constQuery, { sort }).then(items => {
+            setItems(items);
+        });
+    }, [constQuery, constStore, sorted.column, sorted.descending]);
+
+    React.useEffect(() => {
+        refreshTable();
+    }, [refreshTable]);
+
+    React.useEffect(() => {
+        constStore.setData(data);
+        refreshTable();
+    }, [constStore, data, refreshTable]);
 
     const fluentColumns: IColumn[] = React.useMemo(() => {
         return columnsAdapter(constColumns, sorted);
     }, [constColumns, sorted]);
 
     const onColumnClick = React.useCallback((event: React.MouseEvent<HTMLElement>, column: IColumn) => {
+        if (constColumns[column.key]?.sortable === false) return;
+
         let sorted = column.isSorted;
         let isSortedDescending: boolean = column.isSortedDescending;
         if (!sorted) {
@@ -105,16 +140,14 @@ export function useFluentGrid({ store, query = {}, sort = [], columns, getSelect
             column: sorted ? column.key : "",
             descending: sorted ? isSortedDescending : false
         });
-    }, []);
-
-    const [items, setItems] = React.useState<any[]>([]);
+    }, [constColumns]);
 
     React.useEffect(() => {
         const sort = sorted.column ? [{ attribute: sorted.column, descending: sorted.descending }] : undefined;
-        store.query(constQuery, { sort }).then(items => {
+        constStore.query(constQuery, { sort }).then(items => {
             setItems(items);
         });
-    }, [constQuery, sorted.column, sorted.descending, store, store.dataVersion]);
+    }, [constQuery, constStore, sorted.column, sorted.descending]);
 
     const selectionHandler = useConst(new Selection({
         onSelectionChanged: () => {
@@ -122,6 +155,16 @@ export function useFluentGrid({ store, query = {}, sort = [], columns, getSelect
         }
     }));
 
+    const renderDetailsHeader = React.useCallback((props: IDetailsHeaderProps, defaultRender?: any) => {
+        return defaultRender({
+            ...props,
+            onRenderColumnHeaderTooltip: (tooltipHostProps) => {
+                return <TooltipHost {...tooltipHostProps} content={tooltipHostProps?.column?.data?.headerTooltip ?? ""} />;
+            },
+            styles: { root: { paddingTop: 1 } }
+        });
+    }, []);
+
     const renderItemColumn = React.useCallback((item: any, index: number, column: IColumn) => {
         if (constColumns[column.key].formatter) {
             return <span style={{ display: "flex" }}>{constColumns[column.key].formatter(item[column.key], item)}</span>;
@@ -140,7 +183,9 @@ export function useFluentGrid({ store, query = {}, sort = [], columns, getSelect
         selectionPreservedOnEmptyClick={true}
         onItemInvoked={this._onItemInvoked}
         onColumnHeaderClick={onColumnClick}
-    />, [fluentColumns, items, onColumnClick, renderItemColumn, selectionHandler]);
+        onRenderDetailsHeader={renderDetailsHeader}
+
+    />, [fluentColumns, items, onColumnClick, renderDetailsHeader, renderItemColumn, selectionHandler]);
 
     const copyButtons = React.useMemo((): ICommandBarItemProps[] => [
         ...createCopyDownloadSelection(constColumns, selection, `${filename}.csv`)

+ 5 - 1
esp/src/webpack.config.js

@@ -95,7 +95,11 @@ module.exports = function (env) {
         resolve: {
             alias: {
                 "clipboard": path.resolve(__dirname, "node_modules/clipboard/dist/clipboard")
-            }
+            },
+            // WebPack >= v5
+            // fallback: {
+            //     "@hpcc-js": path.resolve(__dirname, "../../../hpcc-js/packages")
+            // },
         },
         plugins: plugins,
         resolveLoader: {