浏览代码

Merge pull request #15233 from GordonSmith/LOGGER

HPCC-26329:  Add logging framework to ECL Watch

Reviewed-By: Jeremy Clements <jeremy.clements@lexisnexisrisk.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 3 年之前
父节点
当前提交
9844ba8ce2

文件差异内容过多而无法显示
+ 618 - 248
esp/src/package-lock.json


+ 3 - 3
esp/src/package.json

@@ -37,9 +37,9 @@
     "@fluentui/react-cards": "1.0.0-beta.0",
     "@fluentui/react-hooks": "^8.2.4",
     "@fluentui/react-icons-mdl2": "^1.1.6",
-    "@hpcc-js/chart": "^2.62.0",
-    "@hpcc-js/codemirror": "^2.44.0",
-    "@hpcc-js/common": "^2.52.0",
+    "@hpcc-js/chart": "^2.65.0",
+    "@hpcc-js/codemirror": "^2.47.0",
+    "@hpcc-js/common": "^2.55.0",
     "@hpcc-js/comms": "^2.49.0",
     "@hpcc-js/dataflow": "^3.0.1",
     "@hpcc-js/eclwatch": "^2.47.0",

+ 8 - 5
esp/src/src-react/components/DFUWorkunitDetails.tsx

@@ -1,5 +1,6 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Pivot, PivotItem, Sticky, StickyPositionType } from "@fluentui/react";
+import { scopedLogger } from "@hpcc-js/util";
 import { SizeMe } from "react-sizeme";
 import nlsHPCC from "src/nlsHPCC";
 import * as FileSpray from "src/FileSpray";
@@ -11,6 +12,8 @@ import { ShortVerticalDivider } from "./Common";
 import { TableGroup } from "./forms/Groups";
 import { XMLSourceEditor } from "./SourceEditor";
 
+const logger = scopedLogger("src-react/components/DFUWorkunitDetails.tsx");
+
 interface DFUWorkunitDetailsProps {
     wuid: string;
     tab?: string;
@@ -30,9 +33,9 @@ export const DFUWorkunitDetails: React.FunctionComponent<DFUWorkunitDetailsProps
 
     React.useEffect(() => {
         setWorkunit(ESPDFUWorkunit.Get(wuid));
-        FileSpray.GetDFUWorkunit({ request: { wuid }}).then(response => {
+        FileSpray.GetDFUWorkunit({ request: { wuid } }).then(response => {
             setDfuWuData(response?.GetDFUWorkunitResponse?.result);
-        });
+        }).catch(logger.error);
     }, [wuid]);
 
     React.useEffect(() => {
@@ -43,9 +46,9 @@ export const DFUWorkunitDetails: React.FunctionComponent<DFUWorkunitDetailsProps
 
     React.useEffect(() => {
         if (!workunit) return;
-        workunit?.fetchXML(function (response) {
+        workunit?.fetchXML().then(response => {
             setWuXML(response);
-        });
+        }).catch(logger.error);
     }, [workunit]);
 
     const canSave = dfuWuData && (
@@ -146,7 +149,7 @@ export const DFUWorkunitDetails: React.FunctionComponent<DFUWorkunitDetailsProps
                             setProtected(value);
                             break;
                         default:
-                            console.log(id, value);
+                            logger.debug(`${id}:  ${value}`);
                     }
                 }} />
                 <hr />

+ 4 - 1
esp/src/src-react/components/Frame.tsx

@@ -1,6 +1,7 @@
 import * as React from "react";
 import { ThemeProvider } from "@fluentui/react";
 import { select as d3Select } from "@hpcc-js/common";
+import { scopedLogger } from "@hpcc-js/util";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { hashHistory } from "../util/history";
 import { router } from "../routes";
@@ -8,6 +9,8 @@ import { darkTheme, lightTheme } from "../themes";
 import { DevTitle } from "./Title";
 import { MainNavigation, SubNavigation } from "./Menu";
 
+const logger = scopedLogger("src-react/components/Frame.tsx");
+
 interface DevFrameProps {
 }
 
@@ -20,7 +23,7 @@ export const DevFrame: React.FunctionComponent<DevFrameProps> = () => {
     React.useEffect(() => {
 
         const unlisten = hashHistory.listen(async (location, action) => {
-            console.log(location.pathname);
+            logger.debug(location.pathname);
             setLocation(location.pathname);
             document.title = `ECL Watch${location.pathname.split("/").join(" | ")}`;
             setBody(await router.resolve(location));

+ 107 - 0
esp/src/src-react/components/LogViewer.tsx

@@ -0,0 +1,107 @@
+import * as React from "react";
+import { Checkbox, 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 { HolyGrail } from "../layouts/HolyGrail";
+import { DojoGrid } from "./DojoGrid";
+import { createCopyDownloadSelection } from "./Common";
+import { useECLWatchLogger } from "../hooks/logging";
+import { Level } from "@hpcc-js/util";
+
+interface LogViewerProps {
+}
+
+export const LogViewer: React.FunctionComponent<LogViewerProps> = ({
+}) => {
+
+    const [errorChecked, setErrorChecked] = React.useState(true);
+    const [warningChecked, setWarningChecked] = React.useState(true);
+    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[] => [
+        { key: "errors", onRender: () => <Checkbox defaultChecked label={`${filterCounts.error || 0} ${nlsHPCC.Errors}`} onChange={(ev, value) => setErrorChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
+        { key: "warnings", onRender: () => <Checkbox defaultChecked label={`${filterCounts.warning || 0} ${nlsHPCC.Warnings}`} onChange={(ev, value) => setWarningChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
+        { key: "infos", onRender: () => <Checkbox defaultChecked label={`${filterCounts.info || 0} ${nlsHPCC.Infos}`} onChange={(ev, value) => setInfoChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> },
+        { key: "others", onRender: () => <Checkbox defaultChecked label={`${filterCounts.other || 0} ${nlsHPCC.Others}`} onChange={(ev, value) => setOtherChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> }
+    ], [filterCounts.error, filterCounts.info, filterCounts.other, filterCounts.warning]);
+
+    const rightButtons = 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();
+        }
+    }, [grid]);
+
+    React.useEffect(() => {
+        const filterCounts = {
+            error: 0,
+            warning: 0,
+            info: 0,
+            other: 0
+        };
+        const filteredExceptions = log.map((row, idx) => {
+            switch (row.level) {
+                case Level.error:
+                    filterCounts.error++;
+                    break;
+                case Level.warning:
+                    filterCounts.warning++;
+                    break;
+                case Level.info:
+                    filterCounts.info++;
+                    break;
+                default:
+                    filterCounts.other++;
+                    break;
+            }
+            return {
+                id: idx,
+                ...row
+            };
+        }).filter(row => {
+            if (!errorChecked && row.level === Level.error) {
+                return false;
+            } else if (!warningChecked && row.level === Level.warning) {
+                return false;
+            } else if (!infoChecked && row.level === Level.info) {
+                return false;
+            } else if (!otherChecked && row.level !== Level.error && row.level !== Level.warning && row.level !== Level.info) {
+                return false;
+            }
+            return true;
+        }).sort((l, r) => {
+            return l.level - r.level;
+        });
+        gridStore.setData(filteredExceptions);
+        refreshTable();
+        setFilterCounts(filterCounts);
+    }, [errorChecked, gridStore, infoChecked, log, otherChecked, refreshTable, warningChecked, lastUpdate]);
+
+    return <HolyGrail
+        header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+        main={
+            <DojoGrid type={"SimpleGrid"} store={gridStore} columns={gridColumns} setGrid={setGrid} setSelection={setSelection} />
+        }
+    />;
+};

+ 1 - 1
esp/src/src-react/components/Menu.tsx

@@ -5,7 +5,7 @@ import nlsHPCC from "src/nlsHPCC";
 import { MainNav, routes } from "../routes";
 import { pushUrl } from "../util/history";
 import { Breadcrumbs } from "./Breadcrumbs";
-import { useFavorites, useHistory } from "src-react/hooks/favorite";
+import { useFavorites, useHistory } from "../hooks/favorite";
 
 //  Top Level Nav  ---
 const navLinkGroups: INavLinkGroup[] = [

+ 39 - 34
esp/src/src-react/components/Title.tsx

@@ -1,43 +1,13 @@
 import * as React from "react";
-import { ContextualMenuItemType, DefaultButton, IconButton, IContextualMenuProps, IIconProps, Image, IPanelProps, IPersonaSharedProps, IRenderFunction, Panel, PanelType, Persona, PersonaSize, SearchBox, Stack, Text, useTheme } from "@fluentui/react";
+import { ContextualMenuItemType, DefaultButton, IconButton, IIconProps, Image, IPanelProps, IPersonaSharedProps, IRenderFunction, Link, Panel, PanelType, Persona, PersonaSize, SearchBox, Stack, Text, useTheme } from "@fluentui/react";
 import { About } from "./About";
 import { useBoolean } from "@fluentui/react-hooks";
 
 import nlsHPCC from "src/nlsHPCC";
+import { useECLWatchLogger } from "src-react/hooks/logging";
 
 const collapseMenuIcon: IIconProps = { iconName: "CollapseMenu" };
 
-const advMenuProps = (setShowAbout): IContextualMenuProps => ({
-    items: [
-        { key: "legacy", text: nlsHPCC.OpenLegacyECLWatch, href: "/esp/files/stub.htm" },
-        { key: "divider_0", itemType: ContextualMenuItemType.Divider },
-        { key: "errors", href: "#/errors", text: nlsHPCC.ErrorWarnings, },
-        { key: "divider_1", itemType: ContextualMenuItemType.Divider },
-        { key: "banner", text: nlsHPCC.SetBanner },
-        { key: "toolbar", text: nlsHPCC.SetToolbar },
-        { key: "divider_2", itemType: ContextualMenuItemType.Divider },
-        { key: "docs", href: "https://hpccsystems.com/training/documentation/", text: nlsHPCC.Documentation, target: "_blank" },
-        { key: "downloads", href: "https://hpccsystems.com/download", text: nlsHPCC.Downloads, target: "_blank" },
-        { key: "releaseNotes", href: "https://hpccsystems.com/download/release-notes", text: nlsHPCC.ReleaseNotes, target: "_blank" },
-        {
-            key: "additionalResources", text: nlsHPCC.AdditionalResources, subMenuProps: {
-                items: [
-                    { key: "redBook", href: "https://wiki.hpccsystems.com/display/hpcc/HPCC+Systems+Red+Book", text: nlsHPCC.RedBook, target: "_blank" },
-                    { key: "forums", href: "https://hpccsystems.com/bb/", text: nlsHPCC.Forums, target: "_blank" },
-                    { key: "issues", href: "https://track.hpccsystems.com/issues/", text: nlsHPCC.IssueReporting, target: "_blank" },
-                ]
-            }
-        },
-        { key: "divider_3", itemType: ContextualMenuItemType.Divider },
-        { key: "lock", text: nlsHPCC.Lock },
-        { key: "logout", text: nlsHPCC.Logout },
-        { key: "divider_4", itemType: ContextualMenuItemType.Divider },
-        { key: "config", href: "#/config", text: nlsHPCC.Configuration },
-        { key: "about", text: nlsHPCC.About, onClick: () => setShowAbout(true) }
-    ],
-    directionalHintFixed: true
-});
-
 const waffleIcon: IIconProps = { iconName: "WaffleOffice365" };
 const searchboxStyles = { margin: "5px", height: "auto", width: "100%" };
 
@@ -69,6 +39,41 @@ export const DevTitle: React.FunctionComponent<DevTitleProps> = ({
         [dismissPanel],
     );
 
+    const [log] = useECLWatchLogger();
+
+    const advMenuProps = React.useMemo(() => {
+        return {
+            items: [
+                { key: "legacy", text: nlsHPCC.OpenLegacyECLWatch, href: "/esp/files/stub.htm" },
+                { key: "divider_0", itemType: ContextualMenuItemType.Divider },
+                { key: "errors", href: "#/log", text: `${nlsHPCC.ErrorWarnings} ${log.length > 0 ? `(${log.length})` : ""}`, },
+                { key: "divider_1", itemType: ContextualMenuItemType.Divider },
+                { key: "banner", text: nlsHPCC.SetBanner },
+                { key: "toolbar", text: nlsHPCC.SetToolbar },
+                { key: "divider_2", itemType: ContextualMenuItemType.Divider },
+                { key: "docs", href: "https://hpccsystems.com/training/documentation/", text: nlsHPCC.Documentation, target: "_blank" },
+                { key: "downloads", href: "https://hpccsystems.com/download", text: nlsHPCC.Downloads, target: "_blank" },
+                { key: "releaseNotes", href: "https://hpccsystems.com/download/release-notes", text: nlsHPCC.ReleaseNotes, target: "_blank" },
+                {
+                    key: "additionalResources", text: nlsHPCC.AdditionalResources, subMenuProps: {
+                        items: [
+                            { key: "redBook", href: "https://wiki.hpccsystems.com/display/hpcc/HPCC+Systems+Red+Book", text: nlsHPCC.RedBook, target: "_blank" },
+                            { key: "forums", href: "https://hpccsystems.com/bb/", text: nlsHPCC.Forums, target: "_blank" },
+                            { key: "issues", href: "https://track.hpccsystems.com/issues/", text: nlsHPCC.IssueReporting, target: "_blank" },
+                        ]
+                    }
+                },
+                { key: "divider_3", itemType: ContextualMenuItemType.Divider },
+                { key: "lock", text: nlsHPCC.Lock },
+                { key: "logout", text: nlsHPCC.Logout },
+                { key: "divider_4", itemType: ContextualMenuItemType.Divider },
+                { key: "config", href: "#/config", text: nlsHPCC.Configuration },
+                { key: "about", text: nlsHPCC.About, onClick: () => setShowAbout(true) }
+            ],
+            directionalHintFixed: true
+        };
+    }, [log.length]);
+
     const theme = useTheme();
 
     return <div style={{ backgroundColor: theme.palette.themeLight }}>
@@ -79,7 +84,7 @@ export const DevTitle: React.FunctionComponent<DevTitleProps> = ({
                         <IconButton iconProps={waffleIcon} onClick={openPanel} style={{ width: 48, height: 48, color: theme.palette.themeDarker }} />
                     </Stack.Item>
                     <Stack.Item align="center">
-                        <Text variant="large" nowrap block ><b style={{ color: theme.palette.themeDarker }}>ECL Watch</b></Text>
+                        <Link href="#/activities"><Text variant="large" nowrap block ><b style={{ color: theme.palette.themeDarker }}>ECL Watch</b></Text></Link>
                     </Stack.Item>
                 </Stack>
             </Stack.Item>
@@ -92,7 +97,7 @@ export const DevTitle: React.FunctionComponent<DevTitleProps> = ({
                         <Persona {...examplePersona} text="Jane Doe" size={PersonaSize.size32} />
                     </Stack.Item>
                     <Stack.Item align="center">
-                        <IconButton title={nlsHPCC.Advanced} iconProps={collapseMenuIcon} menuProps={advMenuProps(setShowAbout)} />
+                        <IconButton title={nlsHPCC.Advanced} iconProps={collapseMenuIcon} menuProps={advMenuProps} />
                     </Stack.Item>
                 </Stack>
             </Stack.Item>

+ 15 - 11
esp/src/src-react/components/WorkunitDetails.tsx

@@ -1,5 +1,6 @@
 import * as React from "react";
 import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Pivot, PivotItem, ScrollablePane, ScrollbarVisibility, Sticky, StickyPositionType } from "@fluentui/react";
+import { scopedLogger } from "@hpcc-js/util";
 import { SizeMe } from "react-sizeme";
 import nlsHPCC from "src/nlsHPCC";
 import { WUStatus } from "src/react/index";
@@ -25,6 +26,8 @@ import { WUXMLSourceEditor } from "./SourceEditor";
 import { Workflows } from "./Workflows";
 import { WorkunitPersona } from "./controls/StateIcon";
 
+const logger = scopedLogger("src-react/components/WorkunitDetails.tsx");
+
 interface WorkunitDetailsProps {
     wuid: string;
     tab?: string;
@@ -84,48 +87,49 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
                     Jobname: jobname,
                     Description: description,
                     Protected: _protected
-                });
+                }).catch(logger.error);
             }
         },
         {
             key: "delete", text: nlsHPCC.Delete, iconProps: { iconName: "Delete" }, disabled: !canDelete,
             onClick: () => {
                 if (confirm(nlsHPCC.YouAreAboutToDeleteThisWorkunit)) {
-                    workunit?.delete();
+                    workunit?.delete().catch(logger.error);
                     pushUrl("/workunits");
                 }
             }
         },
         {
             key: "restore", text: nlsHPCC.Restore, disabled: !workunit?.Archived,
-            onClick: () => workunit?.restore()
+            onClick: () => workunit?.restore().catch(logger.error)
+
         },
         { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "reschedule", text: nlsHPCC.Reschedule, disabled: !canReschedule,
-            onClick: () => workunit?.reschedule()
+            onClick: () => workunit?.reschedule().catch(logger.error)
         },
         {
             key: "deschedule", text: nlsHPCC.Deschedule, disabled: !canDeschedule,
-            onClick: () => workunit?.deschedule()
+            onClick: () => workunit?.deschedule().catch(logger.error)
         },
         { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "setToFailed", text: nlsHPCC.SetToFailed, disabled: workunit?.Archived || workunit?.isComplete() || workunit?.isDeleted(),
-            onClick: () => workunit?.setToFailed()
+            onClick: () => workunit?.setToFailed().catch(logger.error)
         },
         {
             key: "abort", text: nlsHPCC.Abort, disabled: workunit?.Archived || workunit?.isComplete() || workunit?.isDeleted(),
-            onClick: () => workunit?.abort()
+            onClick: () => workunit?.abort().catch(logger.error)
         },
         { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
         {
             key: "recover", text: nlsHPCC.Recover, disabled: workunit?.Archived || !workunit?.isComplete() || workunit?.isDeleted(),
-            onClick: () => workunit?.resubmit()
+            onClick: () => workunit?.resubmit().catch(logger.error)
         },
         {
             key: "resubmit", text: nlsHPCC.Resubmit, disabled: workunit?.Archived || !workunit?.isComplete() || workunit?.isDeleted(),
-            onClick: () => workunit?.resubmit()
+            onClick: () => workunit?.resubmit().catch(logger.error)
         },
         {
             key: "clone", text: nlsHPCC.Clone, disabled: workunit?.Archived || !workunit?.isComplete() || workunit?.isDeleted(),
@@ -134,7 +138,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
                     if (wu && wu.Wuid) {
                         pushUrl(`/workunits/${wu?.Wuid}`);
                     }
-                });
+                }).catch(logger.error);
             }
         },
         { key: "divider_5", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
@@ -213,7 +217,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
                                                     setProtected(value);
                                                     break;
                                                 default:
-                                                    console.log(id, value);
+                                                    logger.debug(`${id}:  ${value}`);
                                             }
                                         }} />
                                     </ScrollablePane>

+ 4 - 0
esp/src/src-react/components/Workunits.tsx

@@ -12,6 +12,9 @@ import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
 import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector } from "./DojoGrid";
+import { scopedLogger } from "@hpcc-js/util";
+
+const logger = scopedLogger("src-react/components/Workunits.tsx");
 
 const FilterFields: Fields = {
     "Type": { type: "checkbox", label: nlsHPCC.ArchivedOnly },
@@ -45,6 +48,7 @@ function formatQuery(_filter) {
             filter.EndDate = new Date(filter.StartDate).toISOString();
         }
     }
+    logger.debug(filter);
     return filter;
 }
 

+ 2 - 2
esp/src/src-react/components/forms/Fields.tsx

@@ -191,7 +191,7 @@ export const TargetClusterTextField: React.FunctionComponent<TargetClusterTextFi
     const [targetClusters, setTargetClusters] = React.useState<IDropdownOption[]>([]);
 
     React.useEffect(() => {
-        const topology = new Topology({ baseUrl: "" });
+        const topology = Topology.attach({ baseUrl: "" });
         topology.fetchLogicalClusters().then((response: TpLogicalClusterQuery.TpLogicalCluster[]) => {
             setTargetClusters(response
                 .map((n, i) => {
@@ -204,7 +204,7 @@ export const TargetClusterTextField: React.FunctionComponent<TargetClusterTextFi
         });
     }, []);
 
-    return <Dropdown { ...props } options={targetClusters} />;
+    return <Dropdown {...props} options={targetClusters} />;
 };
 
 export interface TargetDropzoneTextFieldProps extends DropdownProps {

+ 51 - 50
esp/src/src-react/components/forms/PublishQuery.tsx

@@ -1,11 +1,14 @@
 import * as React from "react";
 import { Checkbox, ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField, } from "@fluentui/react";
 import { useId } from "@fluentui/react-hooks";
+import { scopedLogger } from "@hpcc-js/util";
 import { useForm, Controller } from "react-hook-form";
 import nlsHPCC from "src/nlsHPCC";
 import * as FormStyles from "./landing-zone/styles";
 import { useWorkunit } from "../../hooks/Workunit";
 
+const logger = scopedLogger("src-react/components/forms/PublishQuery.tsx");
+
 interface PublishFormValues {
     jobName: string;
     remoteDali: string;
@@ -50,16 +53,14 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
     const onSubmit = React.useCallback(() => {
         handleSubmit(
             (data, evt) => {
-                workunit.publish(data.jobName).then(response => {
-                    workunit.update({ Jobname: data.jobName }).then(response => {
-                        closeForm();
-                        reset(defaultValues);
-                    });
-                });
+                workunit.publish(data.jobName).then(() => {
+                    return workunit.update({ Jobname: data.jobName });
+                }).then(() => {
+                    closeForm();
+                    reset(defaultValues);
+                }).catch(logger.error);
             },
-            err => {
-                console.log(err);
-            }
+            logger.info
         )();
     }, [closeForm, handleSubmit, reset, workunit]);
 
@@ -111,13 +112,13 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        required={true}
-                        label={nlsHPCC.JobName}
-                        value={value}
-                        errorMessage={ error && error?.message }
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            required={true}
+                            label={nlsHPCC.JobName}
+                            value={value}
+                            errorMessage={error && error?.message}
+                        />}
                     rules={{
                         required: nlsHPCC.ValidationErrorRequired
                     }}
@@ -128,12 +129,12 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.RemoteDali}
-                        value={value}
-                        errorMessage={ error && error?.message }
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.RemoteDali}
+                            value={value}
+                            errorMessage={error && error?.message}
+                        />}
                 />
                 <Controller
                     control={control} name="sourceProcess"
@@ -141,12 +142,12 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.SourceProcess}
-                        value={value}
-                        errorMessage={ error && error?.message }
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.SourceProcess}
+                            value={value}
+                            errorMessage={error && error?.message}
+                        />}
                 />
                 <Controller
                     control={control} name="comment"
@@ -154,11 +155,11 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.Comment}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.Comment}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="priority"
@@ -166,34 +167,34 @@ export const PublishQueryForm: React.FunctionComponent<PublishFormProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <Dropdown
-                        key={fieldName}
-                        label={nlsHPCC.Priority}
-                        options={[
-                            { key: "", text: nlsHPCC.None },
-                            { key: "SLA", text: nlsHPCC.SLA },
-                            { key: "Low", text: nlsHPCC.Low },
-                            { key: "High", text: nlsHPCC.High },
-                        ]}
-                        selectedKey={value}
-                        onChange={(evt, option) => {
-                            onChange(option.key);
-                        }}
-                    /> }
+                            key={fieldName}
+                            label={nlsHPCC.Priority}
+                            options={[
+                                { key: "", text: nlsHPCC.None },
+                                { key: "SLA", text: nlsHPCC.SLA },
+                                { key: "Low", text: nlsHPCC.Low },
+                                { key: "High", text: nlsHPCC.High },
+                            ]}
+                            selectedKey={value}
+                            onChange={(evt, option) => {
+                                onChange(option.key);
+                            }}
+                        />}
                 />
                 <div style={{ paddingTop: "15px" }}>
                     <Controller
                         control={control} name="allowForeignFiles"
                         render={({
-                            field : { onChange, name: fieldName, value }
-                        }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.AllowForeignFiles} /> }
+                            field: { onChange, name: fieldName, value }
+                        }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.AllowForeignFiles} />}
                     />
                 </div>
                 <div style={{ paddingTop: "10px" }}>
                     <Controller
                         control={control} name="updateSuperFiles"
                         render={({
-                            field : { onChange, name: fieldName, value }
-                        }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.UpdateSuperFiles} /> }
+                            field: { onChange, name: fieldName, value }
+                        }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.UpdateSuperFiles} />}
                     />
                 </div>
             </Stack>

+ 32 - 31
esp/src/src-react/components/forms/SlaveLogs.tsx

@@ -1,11 +1,14 @@
 import * as React from "react";
 import { ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField, } from "@fluentui/react";
 import { useId } from "@fluentui/react-hooks";
+import { scopedLogger } from "@hpcc-js/util";
 import { useForm, Controller } from "react-hook-form";
 import nlsHPCC from "src/nlsHPCC";
 import * as FormStyles from "./landing-zone/styles";
 import { useWorkunit } from "../../hooks/Workunit";
 
+const logger = scopedLogger("src-react/components/forms/SlaveLogs.tsx");
+
 interface SlaveLogsValues {
     ThorProcess: string;
     SlaveNumber: string;
@@ -53,9 +56,7 @@ export const SlaveLogs: React.FunctionComponent<SlaveLogsProps> = ({
                 closeForm();
                 reset(defaultValues);
             },
-            err => {
-                console.log(err);
-            }
+            logger.info
         )();
     }, [closeForm, clusterGroup, handleSubmit, reset, thorLogDate, wuid]);
 
@@ -111,15 +112,15 @@ export const SlaveLogs: React.FunctionComponent<SlaveLogsProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <Dropdown
-                        key={fieldName}
-                        label={nlsHPCC.ThorProcess}
-                        options={thorProcesses}
-                        required={true}
-                        onChange={(evt, option) => {
-                            onChange(option.key);
-                        }}
-                        errorMessage={ error && error.message }
-                    /> }
+                            key={fieldName}
+                            label={nlsHPCC.ThorProcess}
+                            options={thorProcesses}
+                            required={true}
+                            onChange={(evt, option) => {
+                                onChange(option.key);
+                            }}
+                            errorMessage={error && error.message}
+                        />}
                     rules={{
                         required: nlsHPCC.ValidationErrorRequired
                     }}
@@ -127,15 +128,15 @@ export const SlaveLogs: React.FunctionComponent<SlaveLogsProps> = ({
                 <Controller
                     control={control} name="SlaveNumber"
                     render={({
-                        field: { onChange, name: fieldName, value},
+                        field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.SlaveNumber}
-                        value={value}
-                        errorMessage={ error && error.message }
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.SlaveNumber}
+                            value={value}
+                            errorMessage={error && error.message}
+                        />}
                     rules={{
                         pattern: {
                             value: /^[1-9]+$/i,
@@ -157,18 +158,18 @@ export const SlaveLogs: React.FunctionComponent<SlaveLogsProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <Dropdown
-                        key={fieldName}
-                        label={nlsHPCC.File}
-                        options={[
-                            { key: "1", text: nlsHPCC.OriginalFile },
-                            { key: "2", text: nlsHPCC.Zip },
-                            { key: "3", text: nlsHPCC.GZip },
-                        ]}
-                        defaultSelectedKey="1"
-                        onChange={(evt, option) => {
-                            onChange(option.key);
-                        }}
-                    /> }
+                            key={fieldName}
+                            label={nlsHPCC.File}
+                            options={[
+                                { key: "1", text: nlsHPCC.OriginalFile },
+                                { key: "2", text: nlsHPCC.Zip },
+                                { key: "3", text: nlsHPCC.GZip },
+                            ]}
+                            defaultSelectedKey="1"
+                            onChange={(evt, option) => {
+                                onChange(option.key);
+                            }}
+                        />}
                 />
             </Stack>
             <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>

+ 114 - 111
esp/src/src-react/components/forms/ZAPDialog.tsx

@@ -2,10 +2,13 @@ import * as React from "react";
 import { Checkbox, ContextualMenu, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField, } from "@fluentui/react";
 import { useId } from "@fluentui/react-hooks";
 import { useForm, Controller } from "react-hook-form";
+import { scopedLogger } from "@hpcc-js/util";
 import * as WsWorkunits from "src/WsWorkunits";
 import * as FormStyles from "./landing-zone/styles";
 import nlsHPCC from "src/nlsHPCC";
 
+const logger = scopedLogger("src-react/components/forms/ZAPDialog.tsx");
+
 interface ZAPDialogValues {
     ZAPFileName: string;
     Wuid: string;
@@ -76,33 +79,33 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                     method: "POST",
                     body: formData
                 })
-                .then(async response => ({
-                    filename: response.headers.get("Content-Disposition"),
-                    blob: await response.blob()
-                }))
-                .then(file => {
-                    let filename = "";
-                    const headers = file.filename.split(";");
-                    for (const header of headers) {
-                        if (header.trim().indexOf("filename=") > -1) {
-                            filename = header.replace("filename=", "");
+                    .then(async response => ({
+                        filename: response.headers.get("Content-Disposition"),
+                        blob: await response.blob()
+                    }))
+                    .then(file => {
+                        let filename = "";
+                        const headers = file.filename.split(";");
+                        for (const header of headers) {
+                            if (header.trim().indexOf("filename=") > -1) {
+                                filename = header.replace("filename=", "");
+                            }
                         }
-                    }
-                    const urlObj = window.URL.createObjectURL(file.blob);
+                        const urlObj = window.URL.createObjectURL(file.blob);
 
-                    const link = document.createElement("a");
-                    link.href = urlObj;
-                    link.download = filename;
-                    link.click();
-                    link.remove();
+                        const link = document.createElement("a");
+                        link.href = urlObj;
+                        link.download = filename;
+                        link.click();
+                        link.remove();
 
-                    closeForm();
-                    reset(defaultValues);
-                });
+                        closeForm();
+                        reset(defaultValues);
+                    })
+                    .catch(logger.error)
+                    ;
             },
-            err => {
-                console.log(err);
-            }
+            logger.info
         )();
     }, [closeForm, handleSubmit, reset]);
 
@@ -133,7 +136,7 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                 }
             }
             reset(newValues);
-        });
+        }).catch(logger.error);
     }, [wuid, reset]);
 
     return <Modal
@@ -160,33 +163,33 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.FileName}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.FileName}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="Wuid"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.WUID}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.WUID}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="BuildVersion"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.ESPBuildVersion}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.ESPBuildVersion}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="ESPIPAddress"
@@ -194,11 +197,11 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.ESPNetworkAddress}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.ESPNetworkAddress}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="ThorIPAddress"
@@ -206,83 +209,83 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                         field: { onChange, name: fieldName, value },
                         fieldState: { error }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.ThorNetworkAddress}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.ThorNetworkAddress}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="ProblemDescription"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.Description}
-                        multiline={true}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.Description}
+                            multiline={true}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="WhatChanged"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.History}
-                        multiline={true}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.History}
+                            multiline={true}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="WhereSlow"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.Timings}
-                        multiline={true}
-                        value={value}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.Timings}
+                            multiline={true}
+                            value={value}
+                        />}
                 />
                 <Controller
                     control={control} name="Password"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.PasswordOpenZAP}
-                        value={value}
-                        type="password"
-                        canRevealPassword={true}
-                        revealPasswordAriaLabel={nlsHPCC.ShowPassword}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.PasswordOpenZAP}
+                            value={value}
+                            type="password"
+                            canRevealPassword={true}
+                            revealPasswordAriaLabel={nlsHPCC.ShowPassword}
+                        />}
                 />
                 <div style={{ padding: "15px 0 7px 0" }}>
                     <div>
                         <Controller
                             control={control} name="IncludeThorSlaveLog"
                             render={({
-                                field : { onChange, name: fieldName, value }
-                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.IncludeSlaveLogs} /> }
+                                field: { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.IncludeSlaveLogs} />}
                         />
                     </div>
                     <div style={{ paddingTop: "10px" }}>
                         <Controller
                             control={control} name="SendEmail"
                             render={({
-                                field : { onChange, name: fieldName, value }
+                                field: { onChange, name: fieldName, value }
                             }) => <Checkbox
-                                name={fieldName}
-                                checked={value}
-                                onChange={onChange}
-                                label={nlsHPCC.SendEmail}
-                                disabled={emailDisabled}
-                            /> }
+                                    name={fieldName}
+                                    checked={value}
+                                    onChange={onChange}
+                                    label={nlsHPCC.SendEmail}
+                                    disabled={emailDisabled}
+                                />}
                         />
                     </div>
 
@@ -292,51 +295,51 @@ export const ZAPDialog: React.FunctionComponent<ZAPDialogProps> = ({
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.EmailTo}
-                        value={value}
-                        placeholder={nlsHPCC.SeeConfigurationManager}
-                        disabled={emailDisabled}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.EmailTo}
+                            value={value}
+                            placeholder={nlsHPCC.SeeConfigurationManager}
+                            disabled={emailDisabled}
+                        />}
                 />
                 <Controller
                     control={control} name="EmailFrom"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.EmailFrom}
-                        value={value}
-                        placeholder={nlsHPCC.SeeConfigurationManager}
-                        disabled={emailDisabled}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.EmailFrom}
+                            value={value}
+                            placeholder={nlsHPCC.SeeConfigurationManager}
+                            disabled={emailDisabled}
+                        />}
                 />
                 <Controller
                     control={control} name="EmailSubject"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.EmailSubject}
-                        value={value}
-                        disabled={emailDisabled}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.EmailSubject}
+                            value={value}
+                            disabled={emailDisabled}
+                        />}
                 />
                 <Controller
                     control={control} name="EmailBody"
                     render={({
                         field: { onChange, name: fieldName, value }
                     }) => <TextField
-                        name={fieldName}
-                        onChange={onChange}
-                        label={nlsHPCC.EmailBody}
-                        multiline={true}
-                        value={value}
-                        disabled={emailDisabled}
-                    /> }
+                            name={fieldName}
+                            onChange={onChange}
+                            label={nlsHPCC.EmailBody}
+                            multiline={true}
+                            value={value}
+                            disabled={emailDisabled}
+                        />}
                 />
             </Stack>
             <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>

+ 6 - 4
esp/src/src-react/hooks/File.ts

@@ -1,8 +1,11 @@
 import * as React from "react";
 import { LogicalFile } from "@hpcc-js/comms";
+import { scopedLogger } from "@hpcc-js/util";
 import * as WsDfu from "src/WsDfu";
 import { useCounter } from "./Workunit";
 
+const logger = scopedLogger("src-react/hooks/file.ts");
+
 export function useFile(cluster: string, name: string): [LogicalFile, number, () => void] {
 
     const [file, setFile] = React.useState<LogicalFile>();
@@ -14,7 +17,7 @@ export function useFile(cluster: string, name: string): [LogicalFile, number, ()
         file.fetchInfo().then(response => {
             setFile(file);
             setLastUpdate(Date.now());
-        });
+        }).catch(logger.error);
     }, [cluster, name, count]);
 
     return [file, lastUpdate, increment];
@@ -29,10 +32,9 @@ export function useDefFile(cluster: string, name: string, format: "def" | "xml")
                 { "request": { "Name": name, "Format": format }
             }).then(response => {
                 setFile(response);
-            });
+            }).catch(logger.error);
         }
-    }, [cluster, name]);
+    }, [cluster, format, name]);
 
     return [file];
 }
-

+ 12 - 9
esp/src/src-react/hooks/Workunit.ts

@@ -1,9 +1,12 @@
 import * as React from "react";
 import { useConst } from "@fluentui/react-hooks";
 import { Workunit, Result, WUStateID, WUInfo, WorkunitsService } from "@hpcc-js/comms";
+import { scopedLogger } from "@hpcc-js/util";
 import nlsHPCC from "src/nlsHPCC";
 import * as Utility from "src/Utility";
 
+const logger = scopedLogger("src-react/hooks/workunit.ts");
+
 export function useCounter(): [number, () => void] {
 
     const [counter, setCounter] = React.useState(0);
@@ -26,7 +29,7 @@ export function useWorkunit(wuid: string, full: boolean = false): [Workunit, WUS
                     wu.refresh(true).then(() => {
                         setWorkunit(wu);
                         setState(wu.StateID);
-                    });
+                    }).catch(logger.error);
                 } else {
                     setState(wu.StateID);
                 }
@@ -51,7 +54,7 @@ export function useWorkunitResults(wuid: string): [Result[], Workunit, WUStateID
     React.useEffect(() => {
         workunit?.fetchResults().then(results => {
             setResults(results);
-        });
+        }).catch(logger.error);
     }, [workunit, state]);
 
     return [results, workunit, state];
@@ -108,7 +111,7 @@ export function useWorkunitVariables(wuid: string): [Variable[], Workunit, WUSta
                 };
             }) || [];
             setVariables([...vars, ...appData, ...debugData]);
-        });
+        }).catch(logger.error);
     }, [workunit, state]);
 
     return [variables, workunit, state];
@@ -141,7 +144,7 @@ export function useWorkunitSourceFiles(wuid: string): [SourceFile[], Workunit, W
                 });
             });
             setSourceFiles(sourceFiles);
-        });
+        }).catch(logger.error);
     }, [workunit, state]);
 
     return [sourceFiles, workunit, state];
@@ -158,7 +161,7 @@ export function useWorkunitWorkflows(wuid: string): [WUInfo.ECLWorkflow[], Worku
             IncludeWorkflows: true
         }).then(response => {
             setWorkflows(response?.Workunit?.Workflows?.ECLWorkflow || []);
-        });
+        }).catch(logger.error);
     }, [workunit, state, count]);
 
     return [workflows, workunit, increment];
@@ -176,7 +179,7 @@ export function useWorkunitXML(wuid: string): [string] {
             Type: "XML"
         }).then(response => {
             setXML(response);
-        });
+        }).catch(logger.error);
     }, [wuid, service]);
 
     return [xml];
@@ -194,7 +197,7 @@ export function useWorkunitExceptions(wuid: string): [WUInfo.ECLException[], Wor
             IncludeExceptions: true
         }).then(response => {
             setExceptions(response?.Workunit?.Exceptions?.ECLException || []);
-        });
+        }).catch(logger.error);
     }, [workunit, state, count]);
 
     return [exceptions, workunit, increment];
@@ -210,7 +213,7 @@ export function useWorkunitResources(wuid: string): [string[], Workunit, WUState
             IncludeResourceURLs: true
         }).then(response => {
             setResources(response?.Workunit?.ResourceURLs?.URL || []);
-        });
+        }).catch(logger.error);
     }, [workunit, state]);
 
     return [resources, workunit, state];
@@ -282,7 +285,7 @@ export function useWorkunitHelpers(wuid: string): [HelperRow[]] {
             ...mapHelpers(workunit, response?.Workunit?.Helpers?.ECLHelpFile),
             ...mapThorLogInfo(workunit, response?.Workunit?.ThorLogList?.ThorLogInfo)
             ]);
-        });
+        }).catch(logger.error);
     }, [workunit, state]);
 
     return [helpers];

+ 1 - 1
esp/src/src-react/hooks/favorite.ts

@@ -2,7 +2,7 @@ import * as React from "react";
 import { CallbackFunction, Observable } from "@hpcc-js/util";
 import { userKeyValStore } from "src/KeyValStore";
 import { IContextualMenuItem } from "@fluentui/react";
-import { hashHistory } from "src-react/util/history";
+import { hashHistory } from "../util/history";
 
 const STORE_FAVORITES_ID = "favorites";
 const STORE_CACHE_TIMEOUT = 10000;

+ 89 - 0
esp/src/src-react/hooks/logging.ts

@@ -0,0 +1,89 @@
+import * as React from "react";
+import { useConst } from "@fluentui/react-hooks";
+import { ESPExceptions, isExceptions } from "@hpcc-js/comms";
+import { Observable, Level, logger as utilLogger, scopedLogger, Writer, CallbackFunction } from "@hpcc-js/util";
+
+const logger = scopedLogger("src-react/util/logging.ts");
+
+let g_logger: ECLWatchLogger;
+
+interface LogEntry {
+    dateTime: string;
+    level: Level;
+    id: string;
+    message: string;
+}
+
+export class ECLWatchLogger implements Writer {
+
+    protected _origWriter: Writer;
+    protected _log: LogEntry[] = [];
+    protected _observable = new Observable("added");
+
+    static init(): ECLWatchLogger {
+        if (g_logger) {
+            logger.error("ECLWatchLogger singleton already initialised.");
+        }
+        return ECLWatchLogger.attach();
+    }
+
+    static attach(): ECLWatchLogger {
+        if (!g_logger) {
+            logger.error("ECLWatchLogger init not called.");
+            g_logger = new ECLWatchLogger();
+        }
+        return g_logger;
+    }
+
+    private constructor() {
+        if (location?.search?.split("DEBUG_LOGGING").length > 1) {
+            utilLogger.level(Level.debug);
+        }
+        this._origWriter = utilLogger.writer();
+        utilLogger.writer(this);
+    }
+
+    log(): Readonly<LogEntry[]> {
+        return this._log;
+    }
+
+    listen(callback: CallbackFunction): () => void {
+        const added = this._observable.addObserver("added", val => callback("added", val));
+        return () => {
+            added.release();
+        };
+    }
+
+    rawWrite(dateTime: string, level: Level, id: string, _msg: string | object): void {
+        let message: string = "";
+        if (_msg instanceof ESPExceptions) {
+            message = _msg.message;
+        } else if (isExceptions(_msg)) {
+            message = "isExceptions";
+        } else if (_msg instanceof Error) {
+            message = _msg.message;
+        } else if (typeof _msg !== "string") {
+            message = JSON.stringify(_msg, undefined, 2);
+        } else if (typeof _msg === "string") {
+            message = _msg;
+        }
+        this._origWriter.write(dateTime, level, id, message);
+        const row = { dateTime, level, id, message };
+        this._log.push(row);
+        this._observable.dispatchEvent("added", row);
+    }
+}
+
+export function useECLWatchLogger(): [Readonly<LogEntry[]>, number] {
+
+    const eclLogger = useConst(() => ECLWatchLogger.attach());
+    const [lastUpdate, setLastUpdate] = React.useState(Date.now());
+
+    React.useEffect(() => {
+        return eclLogger?.listen((eventID, row) => {
+            setLastUpdate(Date.now());
+        });
+    }, [eclLogger]);
+
+    return [eclLogger.log(), lastUpdate];
+}

+ 13 - 4
esp/src/src-react/index.tsx

@@ -1,15 +1,20 @@
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import { initializeIcons } from "@fluentui/react";
+import { scopedLogger } from "@hpcc-js/util";
 import { initSession } from "src/Session";
+import { ECLWatchLogger } from "./hooks/logging";
 
 import "css!dojo-themes/flat/flat.css";
 import "css!hpcc/css/ecl.css";
 import "css!hpcc/css/hpcc.css";
 import "src-react-css/index.css";
 
+ECLWatchLogger.init();
 initializeIcons();
 
+const logger = scopedLogger("src-react/index.tsx");
+
 declare const dojoConfig: any;
 
 const baseHost = "";
@@ -28,8 +33,12 @@ dojoConfig.disableLegacyHashing = true;
 initSession();
 
 import("./components/Frame").then(_ => {
-    ReactDOM.render(
-        <_.DevFrame />,
-        document.getElementById("placeholder")
-    );
+    try {
+        ReactDOM.render(
+            <_.DevFrame />,
+            document.getElementById("placeholder")
+        );
+    } catch (e) {
+        logger.error(e);
+    }
 });

文件差异内容过多而无法显示
+ 7 - 3
esp/src/src-react/routes.tsx


+ 11 - 10
esp/src/src-react/util/history.ts

@@ -1,8 +1,10 @@
 import UniversalRouter, { ResolveContext } from "universal-router";
 import { parse, ParsedQuery, stringify } from "query-string";
-import { hashSum } from "@hpcc-js/util";
+import { hashSum, scopedLogger } from "@hpcc-js/util";
 import { userKeyValStore } from "src/KeyValStore";
 
+const logger = scopedLogger("src-react/util/history.ts");
+
 let g_router: UniversalRouter;
 
 export function initialize(routes) {
@@ -74,7 +76,7 @@ class History<S extends object = object> {
         });
 
         window.addEventListener("popstate", ev => {
-            console.log("popstate: " + document.location + ", state: " + JSON.stringify(ev.state));
+            logger.debug("popstate: " + document.location + ", state: " + JSON.stringify(ev.state));
             this.state = ev.state;
         });
 
@@ -85,8 +87,7 @@ class History<S extends object = object> {
                     this._recent = retVal;
                 }
             }
-        }).catch(e => {
-        }).finally(() => {
+        }).catch(logger.error).finally(() => {
             this._recent = this._recent === undefined ? [] : this._recent;
         });
     }
@@ -122,12 +123,12 @@ class History<S extends object = object> {
 
     updateRecent() {
         if (this._recent !== undefined) {
-        this._recent = this._recent?.filter(row => row.id !== this.location.id) || [];
-        this._recent.unshift(this.location);
-        if (this._recent.length > 10) {
-            this._recent.length = 10;
-        }
-            this._store.set(STORE_HISTORY_ID, JSON.stringify(this._recent));
+            this._recent = this._recent?.filter(row => row.id !== this.location.id) || [];
+            this._recent.unshift(this.location);
+            if (this._recent.length > 10) {
+                this._recent.length = 10;
+            }
+            this._store.set(STORE_HISTORY_ID, JSON.stringify(this._recent)).catch(logger.error);
         }
     }
 

+ 6 - 3
esp/src/src/ESPDFUWorkunit.ts

@@ -180,13 +180,16 @@ const Workunit = declare([ESPUtil.Singleton, ESPUtil.Monitor], { // jshint ignor
     },
     submit(target) {
     },
-    fetchXML(onFetchXML) {
-        FileSpray.DFUWUFile({
+    fetchXML(onFetchXML?) {
+        return FileSpray.DFUWUFile({
             request: {
                 Wuid: this.Wuid
             }
         }).then(function (response) {
-            onFetchXML(response);
+            if (onFetchXML) {
+                onFetchXML(response);
+            }
+            return response;
         });
     },
     _resubmit(clone, resetWorkflow, callback) {