Browse Source

Merge pull request #14691 from GordonSmith/HPCC-25509-WUDetailsResources

HPCC-25509 Add React WU Resources

Reviewed-By: Jeremy Clements <jeremy.clements@lexisnexisrisk.com>
Reviewed-By: Jaman Brundage <jaman.brundage@lexisnexisrisk.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 4 years ago
parent
commit
b190e01a92

+ 12 - 0
esp/src/src-react/components/IFrame.tsx

@@ -0,0 +1,12 @@
+import * as React from "react";
+
+interface IFrameProps {
+    src: string;
+}
+
+export const IFrame: React.FunctionComponent<IFrameProps> = ({
+    src
+}) => {
+
+    return <iframe src={src} width="100%" height="100%" style={{ border: "none" }} ></iframe>;
+};

+ 129 - 0
esp/src/src-react/components/Resources.tsx

@@ -0,0 +1,129 @@
+import * as React from "react";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
+import { useConst } from "@fluentui/react-hooks";
+import { AlphaNumSortMemory } from "src/Memory";
+import * as Observable from "dojo/store/Observable";
+import nlsHPCC from "src/nlsHPCC";
+import { useWorkunitResources } from "../hooks/Workunit";
+import { HolyGrail } from "../layouts/HolyGrail";
+import { ShortVerticalDivider } from "./Common";
+import { DojoGrid, selector } from "./DojoGrid";
+
+const defaultUIState = {
+    hasSelection: false
+};
+
+interface ResourcesProps {
+    wuid: string;
+}
+
+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);
+
+    //  Command Bar  ---
+    const buttons: ICommandBarItemProps[] = [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "open", text: nlsHPCC.Open, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/iframe?src=${encodeURIComponent(`WsWorkunits/${selection[0].URL}`)}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/iframe?src=${encodeURIComponent(`WsWorkunits/${selection[i].URL}`)}`, "_blank");
+                    }
+                }
+            }
+        },
+        {
+            key: "content", text: nlsHPCC.Content, disabled: !uiState.hasSelection, iconProps: { iconName: "WindowEdit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/text?src=${encodeURIComponent(`WsWorkunits/${selection[0].URL}`)}`;
+                } else {
+                    for (let i = selection.length - 1; i >= 0; --i) {
+                        window.open(`#/text?src=${encodeURIComponent(`WsWorkunits/${selection[i].URL}`)}`, "_blank");
+                    }
+                }
+            }
+        },
+    ];
+
+    const rightButtons: ICommandBarItemProps[] = [
+        {
+            key: "copy", text: nlsHPCC.CopyWUIDs, disabled: true, iconOnly: true, iconProps: { iconName: "Copy" },
+            onClick: () => {
+                //  TODO:  HPCC-25473
+            }
+        },
+        {
+            key: "download", text: nlsHPCC.DownloadToCSV, disabled: true, iconOnly: true, iconProps: { iconName: "Download" },
+            onClick: () => {
+                //  TODO:  HPCC-25473
+            }
+        }
+    ];
+
+    //  Grid ---
+    const gridStore = useConst(new Observable(new AlphaNumSortMemory("DisplayPath", { Name: true, Value: true })));
+    const gridQuery = useConst({});
+    const gridSort = useConst([{ attribute: "Wuid", "descending": true }]);
+    const gridColumns = useConst({
+        col1: selector({
+            width: 27,
+            selectorType: "checkbox"
+        }),
+        DisplayPath: {
+            label: nlsHPCC.Name, sortable: true,
+            formatter: function (url, row) {
+                return `<a href='#/iframe?src=${encodeURIComponent(`WsWorkunits/${row.URL}`)}' class='dgrid-row-url'>${url}</a>`;
+            }
+        }
+    });
+
+    const refreshTable = (clearSelection = false) => {
+        grid?.set("query", gridQuery);
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    };
+
+    //  Selection  ---
+    React.useEffect(() => {
+        const state = { ...defaultUIState };
+
+        for (let i = 0; i < selection.length; ++i) {
+            state.hasSelection = true;
+            break;
+        }
+        setUIState(state);
+    }, [selection]);
+
+    React.useEffect(() => {
+        gridStore.setData(resources.filter((row, idx) => idx > 0).map(row => {
+            return {
+                URL: row,
+                DisplayPath: row.substring(`res/${wuid}/`.length)
+            };
+        }));
+        refreshTable();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [gridStore, resources]);
+
+    return <HolyGrail
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        main={
+            <DojoGrid store={gridStore} query={gridQuery} sort={gridSort} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+        }
+    />;
+};

+ 55 - 5
esp/src/src-react/components/SourceEditor.tsx

@@ -1,7 +1,7 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
 import { useConst } from "@fluentui/react-hooks";
-import { XMLEditor } from "@hpcc-js/codemirror";
+import { Editor, XMLEditor } from "@hpcc-js/codemirror";
 import nlsHPCC from "src/nlsHPCC";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter";
@@ -9,13 +9,15 @@ import { useWorkunitXML } from "../hooks/Workunit";
 import { ShortVerticalDivider } from "./Common";
 
 interface SourceEditorProps {
-    text: string;
+    text?: string;
     readonly?: boolean;
+    mode?: "xml" | "text";
 }
 
-export const XMLSourceEditor: React.FunctionComponent<SourceEditorProps> = ({
+const SourceEditor: React.FunctionComponent<SourceEditorProps> = ({
     text = "",
-    readonly = false
+    readonly = false,
+    mode = "text"
 }) => {
 
     //  Command Bar  ---
@@ -29,7 +31,7 @@ export const XMLSourceEditor: React.FunctionComponent<SourceEditorProps> = ({
         { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
     ];
 
-    const editor = useConst(new XMLEditor());
+    const editor = useConst(mode === "text" ? new Editor() : new XMLEditor());
     React.useEffect(() => {
         editor
             .text(text)
@@ -47,6 +49,32 @@ export const XMLSourceEditor: React.FunctionComponent<SourceEditorProps> = ({
     />;
 };
 
+interface TextSourceEditorProps {
+    text: string;
+    readonly?: boolean;
+}
+
+export const TextSourceEditor: React.FunctionComponent<TextSourceEditorProps> = ({
+    text = "",
+    readonly = false
+}) => {
+
+    return <SourceEditor text={text} readonly={readonly} mode="text"></SourceEditor>;
+};
+
+interface XMLSourceEditorProps {
+    text: string;
+    readonly?: boolean;
+}
+
+export const XMLSourceEditor: React.FunctionComponent<XMLSourceEditorProps> = ({
+    text = "",
+    readonly = false
+}) => {
+
+    return <SourceEditor text={text} readonly={readonly} mode="xml"></SourceEditor>;
+};
+
 export interface WUXMLSourceEditorProps {
     wuid: string;
 }
@@ -59,3 +87,25 @@ export const WUXMLSourceEditor: React.FunctionComponent<WUXMLSourceEditorProps>
 
     return <XMLSourceEditor text={xml} readonly={true} />;
 };
+
+export interface WUResourceEditorProps {
+    src: string;
+}
+
+export const WUResourceEditor: React.FunctionComponent<WUResourceEditorProps> = ({
+    src
+}) => {
+
+    const [text, setText] = React.useState("");
+
+    React.useEffect(() => {
+        fetch(src).then(response => {
+            return response.text();
+        }).then(content => {
+            setText(content);
+        });
+    }, [src]);
+
+    return <SourceEditor text={text} readonly={true} mode="text"></SourceEditor>;
+};
+

+ 4 - 2
esp/src/src-react/components/WorkunitDetails.tsx

@@ -16,6 +16,7 @@ import { SourceFiles } from "./SourceFiles";
 import { TableGroup } from "./forms/Groups";
 import { InfoGrid } from "./InfoGrid";
 import { Queries } from "./Queries";
+import { Resources } from "./Resources";
 import { WUXMLSourceEditor } from "./SourceEditor";
 import { Workflows } from "./Workflows";
 
@@ -118,6 +119,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
     const protectedImage = getImageURL(workunit?.Protected ? "locked.png" : "unlocked.png");
     const stateIconClass = getStateIconClass(workunit?.StateID, workunit?.isComplete(), workunit?.Archived);
     const serviceNames = workunit?.ServiceNames?.Item?.join("\n") || "";
+    const resourceCount = workunit?.ResourceURLCount > 1 ? workunit?.ResourceURLCount - 1 : undefined;
 
     return <SizeMe monitorHeight>{({ size }) =>
         <Pivot overflowBehavior="menu" style={{ height: "100%" }} selectedKey={tab} onLinkClick={evt => pushUrl(`/workunits/${wuid}/${evt.props.itemKey}`)}>
@@ -201,8 +203,8 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
             <PivotItem headerText={nlsHPCC.Queries} itemIcon="Search" itemKey="queries" style={pivotItemStyle(size, 0)}>
                 <Queries wuid={wuid} />
             </PivotItem>
-            <PivotItem headerText={nlsHPCC.Resources} itemKey="resources" style={pivotItemStyle(size, 0)}>
-                <DojoAdapter widgetClassID="ResourcesWidget" params={{ Wuid: wuid }} />
+            <PivotItem headerText={nlsHPCC.Resources} itemKey="resources" itemCount={resourceCount} style={pivotItemStyle(size, 0)}>
+                <Resources wuid={wuid} />
             </PivotItem>
             <PivotItem headerText={nlsHPCC.Helpers} itemKey="helpers" itemCount={workunit?.HelpersCount} style={pivotItemStyle(size, 0)}>
                 <DojoAdapter widgetClassID="HelpersWidget" params={{ Wuid: wuid }} />

+ 16 - 0
esp/src/src-react/hooks/Workunit.ts

@@ -184,3 +184,19 @@ export function useWorkunitExceptions(wuid: string): [WUInfo.ECLException[], Wor
 
     return [exceptions, workunit, increment];
 }
+
+export function useWorkunitResources(wuid: string): [string[], Workunit, WUStateID] {
+
+    const [workunit, state] = useWorkunit(wuid);
+    const [resources, setResources] = React.useState<string[]>([]);
+
+    React.useEffect(() => {
+        workunit?.fetchInfo({
+            IncludeResourceURLs: true
+        }).then(response => {
+            setResources(response?.Workunit?.ResourceURLs?.URL || []);
+        });
+    }, [workunit, state]);
+
+    return [resources, workunit, state];
+}

File diff suppressed because it is too large
+ 7 - 0
esp/src/src-react/routes.tsx