diff --git a/common/workunit/workunit.cpp b/common/workunit/workunit.cpp index 78b2a1c864b..6d13927f970 100644 --- a/common/workunit/workunit.cpp +++ b/common/workunit/workunit.cpp @@ -3686,7 +3686,8 @@ EnumMapping priorityClasses[] = { const char * getWorkunitStateStr(WUState state) { - dbgassertex(state < WUStateSize); + if (state >= WUStateSize) + return "unknown workunit state"; return states[state].str; // MORE - should be using getEnumText, or need to take steps to ensure values remain contiguous and in order. } @@ -14580,11 +14581,22 @@ void executeThorGraph(const char * graphName, IConstWorkUnit &workunit, const IP } } + // NB: check for expected success state (WUStateWait). If any other state, abort. { Owned w = &workunit.lock(); WUState state = w->getState(); - if (WUStateFailed == state) - throw makeStringException(0, "Workunit failed"); + if (WUStateWait != state) // expected state from successful Thor run from above + { + switch (state) + { + case WUStateAborting: + throw new WorkflowException(0, "Workunit abort requested", 0, WorkflowException::ABORT, MSGAUD_user); + case WUStateFailed: + throw makeStringException(0, "Workunit failed"); + default: + throw makeStringExceptionV(0, "Workunit failed. Unexpected state: %s", getWorkunitStateStr(state)); + } + } w->setState(WUStateRunning); } #else diff --git a/esp/src/eclwatch/WUDetailsWidget.js b/esp/src/eclwatch/WUDetailsWidget.js index 411bba32c52..a6d214c7697 100644 --- a/esp/src/eclwatch/WUDetailsWidget.js +++ b/esp/src/eclwatch/WUDetailsWidget.js @@ -41,6 +41,8 @@ define([ "dijit/form/TextBox", "dijit/Dialog", "dijit/form/SimpleTextarea", + "dijit/form/DateTextBox", + "dijit/form/TimeTextBox", "hpcc/TableContainer" ], function (declare, lang, nlsHPCCMod, dom, domAttr, domClass, topic, diff --git a/esp/src/src-react/components/Logs.tsx b/esp/src/src-react/components/Logs.tsx index 5e697c77b55..3b2276312bd 100644 --- a/esp/src/src-react/components/Logs.tsx +++ b/esp/src/src-react/components/Logs.tsx @@ -230,7 +230,7 @@ export const Logs: React.FunctionComponent = ({ } } const colTypes = logColumns?.map(c => c.LogType.toString()) ?? []; - removeAllExcept(retVal, colTypes); + removeAllExcept(retVal, [...colTypes, "LogLineLimit", "StartDate", "EndDate"]); return retVal; }, [filter, logColumns, wuid]); diff --git a/esp/src/src-react/components/Menu.tsx b/esp/src/src-react/components/Menu.tsx index ea6b9601d00..49c63033893 100644 --- a/esp/src/src-react/components/Menu.tsx +++ b/esp/src/src-react/components/Menu.tsx @@ -299,8 +299,6 @@ export const SubNavigation: React.FunctionComponent = ({ React.useEffect(() => { hasLogAccess().then(response => { setLogsDisabled(!response); - }).catch(() => { - setLogsDisabled(true); }); }, []); const linkStyle = React.useCallback((disabled) => { diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index 0cb9a7aff41..3d1f91cc2bd 100644 --- a/esp/src/src-react/components/Metrics.tsx +++ b/esp/src/src-react/components/Metrics.tsx @@ -15,7 +15,7 @@ import { HolyGrail } from "../layouts/HolyGrail"; import { AutosizeComponent, AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter"; import { DockPanel, DockPanelItem, ResetableDockPanel } from "../layouts/DockPanel"; import { LayoutStatus, MetricGraph, MetricGraphWidget, isGraphvizWorkerResponse, layoutCache } from "../util/metricGraph"; -import { pushUrl } from "../util/history"; +import { pushUrl as _pushUrl } from "../util/history"; import { debounce } from "../util/throttle"; import { ErrorBoundary } from "../util/errorBoundary"; import { ShortVerticalDivider } from "./Common"; @@ -45,6 +45,7 @@ interface MetricsProps { queryId?: string; parentUrl?: string; selection?: string; + fullscreen?: boolean; } export const Metrics: React.FunctionComponent = ({ @@ -52,7 +53,8 @@ export const Metrics: React.FunctionComponent = ({ querySet = "", queryId = "", parentUrl = `/workunits/${wuid}/metrics`, - selection + selection, + fullscreen = false }) => { const [_uiState, _setUIState] = React.useState({ ...defaultUIState }); const [selectedMetricsSource, setSelectedMetricsSource] = React.useState(""); @@ -63,7 +65,6 @@ export const Metrics: React.FunctionComponent = ({ const [showMetricOptions, setShowMetricOptions] = React.useState(false); const [dockpanel, setDockpanel] = React.useState(); const [trackSelection, setTrackSelection] = React.useState(true); - const [fullscreen, setFullscreen] = React.useState(false); const [hotspots, setHotspots] = React.useState(""); const [lineage, setLineage] = React.useState([]); const [selectedLineage, setSelectedLineage] = React.useState(); @@ -96,10 +97,24 @@ export const Metrics: React.FunctionComponent = ({ }).catch(err => logger.error(err)); }, [wuid]); + const pushUrl = React.useCallback((selection?: string, fullscreen?: boolean) => { + const selectionStr = selection?.length ? `/${selection}` : ""; + const fullscreenStr = fullscreen ? "?fullscreen" : ""; + _pushUrl(`${parentUrl}${selectionStr}${fullscreenStr}`); + }, [parentUrl]); + + const pushSelectionUrl = React.useCallback((selection: string) => { + pushUrl(selection, fullscreen); + }, [fullscreen, pushUrl]); + + const pushFullscreenUrl = React.useCallback((fullscreen: boolean) => { + pushUrl(selection, fullscreen); + }, [pushUrl, selection]); + const onHotspot = React.useCallback(() => { setSelectedMetricsSource("hotspot"); - pushUrl(`${parentUrl}/${selection}`); - }, [parentUrl, selection]); + pushSelectionUrl(selection); + }, [pushSelectionUrl, selection]); // Timeline --- const timeline = useConst(() => new WUTimelineNoFetch() @@ -114,11 +129,11 @@ export const Metrics: React.FunctionComponent = ({ timeline.selection([]); setSelectedMetricsSource("scopesTable"); setScopeFilter(`name:${row[7].__hpcc_id}`); - pushUrl(`${parentUrl}/${row[7].id}`); + pushSelectionUrl(row[7].id); } }, true) ; - }, [parentUrl, timeline]); + }, [pushSelectionUrl, timeline]); React.useEffect(() => { if (view.showTimeline) { @@ -142,10 +157,10 @@ export const Metrics: React.FunctionComponent = ({ .on("selectionChanged", () => { const selection = metricGraphWidget.selection().filter(id => metricGraph.item(id)).map(id => metricGraph.item(id).id); setSelectedMetricsSource("metricGraphWidget"); - pushUrl(`${parentUrl}/${selection.join(",")}`); + pushSelectionUrl(selection.join(",")); }, true) ; - }, [metricGraph, metricGraphWidget, parentUrl]); + }, [metricGraph, metricGraphWidget, pushSelectionUrl]); React.useEffect(() => { metricGraph.load(metrics); @@ -301,8 +316,8 @@ export const Metrics: React.FunctionComponent = ({ const scopesSelectionChanged = React.useCallback((source: SelectedMetricsSource, selection: IScope[]) => { setSelectedMetricsSource(source); - pushUrl(`${parentUrl}/${selection.map(row => row.__lparam?.id ?? row.id).join(",")}`); - }, [parentUrl]); + pushSelectionUrl(selection.map(row => row.__lparam?.id ?? row.id).join(",")); + }, [pushSelectionUrl]); const scopesTable = useConst(() => new ScopesTable() .multiSelect(true) @@ -520,9 +535,9 @@ export const Metrics: React.FunctionComponent = ({ { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => }, { key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" }, - onClick: () => setFullscreen(!fullscreen) + onClick: () => pushFullscreenUrl(!fullscreen) } - ], [dot, formatColumns, fullscreen, metrics, wuid]); + ], [dot, formatColumns, fullscreen, metrics, pushFullscreenUrl, wuid]); const setShowMetricOptionsHook = React.useCallback((show: boolean) => { setShowMetricOptions(show); diff --git a/esp/src/src-react/components/Queries.tsx b/esp/src/src-react/components/Queries.tsx index a64847aff70..2a9d05c228d 100644 --- a/esp/src/src-react/components/Queries.tsx +++ b/esp/src/src-react/components/Queries.tsx @@ -134,7 +134,7 @@ export const Queries: React.FunctionComponent = ({ width: 16, sortable: false, formatter: (mixed, row) => { - const mixedStates = row.Clusters.ClusterQueryState[0]?.MixedNodeStates ?? false; + const mixedStates = row?.Clusters?.ClusterQueryState[0]?.MixedNodeStates ?? false; if (mixedStates === true) { return ; } diff --git a/esp/src/src-react/components/WorkunitDetails.tsx b/esp/src/src-react/components/WorkunitDetails.tsx index f30bbf974ae..47d632db043 100644 --- a/esp/src/src-react/components/WorkunitDetails.tsx +++ b/esp/src/src-react/components/WorkunitDetails.tsx @@ -40,7 +40,7 @@ interface WorkunitDetailsProps { parentUrl?: string; tab?: string; state?: { outputs?: string, metrics?: string, resources?: string, helpers?: string, eclsummary?: string }; - queryParams?: { outputs?: StringStringMap, inputs?: StringStringMap, resources?: StringStringMap, helpers?: StringStringMap, logs?: StringStringMap }; + queryParams?: { summary?: StringStringMap, outputs?: StringStringMap, inputs?: StringStringMap, metrics?: StringStringMap, resources?: StringStringMap, helpers?: StringStringMap, logs?: StringStringMap }; } export const WorkunitDetails: React.FunctionComponent = ({ @@ -115,9 +115,6 @@ export const WorkunitDetails: React.FunctionComponent = ({ hasLogAccess().then(response => { setLogsDisabled(!response); return response; - }).catch(err => { - logger.warning(err); - setLogsDisabled(true); }); }, [wuid], [queryParams]); @@ -180,7 +177,7 @@ export const WorkunitDetails: React.FunctionComponent = ({
- + @@ -205,7 +202,7 @@ export const WorkunitDetails: React.FunctionComponent = ({ }> - + diff --git a/esp/src/src-react/components/WorkunitSummary.tsx b/esp/src/src-react/components/WorkunitSummary.tsx index 8e74c0b008c..a519dd1a36c 100644 --- a/esp/src/src-react/components/WorkunitSummary.tsx +++ b/esp/src/src-react/components/WorkunitSummary.tsx @@ -9,6 +9,7 @@ import { useConfirm } from "../hooks/confirm"; import { useWorkunit, useWorkunitExceptions } from "../hooks/workunit"; import { ReflexContainer, ReflexElement, ReflexSplitter } from "../layouts/react-reflex"; import { pushUrl, replaceUrl } from "../util/history"; +import { HolyGrail } from "../layouts/HolyGrail"; import { ShortVerticalDivider } from "./Common"; import { TableGroup } from "./forms/Groups"; import { PublishQueryForm } from "./forms/PublishQuery"; @@ -26,10 +27,12 @@ interface MessageBarContent { interface WorkunitSummaryProps { wuid: string; + fullscreen?: boolean; } export const WorkunitSummary: React.FunctionComponent = ({ - wuid + wuid, + fullscreen = false }) => { const [workunit, , , , refresh] = useWorkunit(wuid, true); @@ -171,6 +174,13 @@ export const WorkunitSummary: React.FunctionComponent = ({ }, ], [_protected, canDelete, canDeschedule, canReschedule, canSave, description, jobname, refresh, refreshSavings, setShowDeleteConfirm, showMessageBar, workunit, wuid]); + const rightButtons = React.useMemo((): ICommandBarItemProps[] => [ + { + key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" }, + onClick: () => pushUrl(`/workunits/${wuid}${fullscreen ? "" : "?fullscreen"}`) + } + ], [fullscreen, wuid]); + const serviceNames = React.useMemo(() => { return workunit?.ServiceNames?.Item?.join("\n") || ""; }, [workunit?.ServiceNames?.Item]); @@ -190,68 +200,70 @@ export const WorkunitSummary: React.FunctionComponent = ({ }, 0) || 0; }, [exceptions]); - return <> - - -
- - - - {messageBarContent && - - {messageBarContent.message} - - } - - - -
- -
-
- 0 ? Math.round((potentialSavings / totalCosts) * 10000) / 100 : 0}%)`, readonly: true }, - "compileCost": { label: nlsHPCC.CompileCost, type: "string", value: `${formatCost(workunit?.CompileCost)}`, readonly: true }, - "executeCost": { label: nlsHPCC.ExecuteCost, type: "string", value: `${formatCost(workunit?.ExecuteCost)}`, readonly: true }, - "fileAccessCost": { label: nlsHPCC.FileAccessCost, type: "string", value: `${formatCost(workunit?.FileAccessCost)}`, readonly: true }, - "protected": { label: nlsHPCC.Protected, type: "checkbox", value: _protected }, - "cluster": { label: nlsHPCC.Cluster, type: "string", value: workunit?.Cluster, readonly: true }, - "totalClusterTime": { label: nlsHPCC.TotalClusterTime, type: "string", value: workunit?.TotalClusterTime ? workunit?.TotalClusterTime : "0.00", readonly: true }, - "abortedBy": { label: nlsHPCC.AbortedBy, type: "string", value: workunit?.AbortBy, readonly: true }, - "abortedTime": { label: nlsHPCC.AbortedTime, type: "string", value: workunit?.AbortTime, readonly: true }, - "ServiceNamesCustom": { label: nlsHPCC.Services, type: "string", value: serviceNames, readonly: true, multiline: true }, - }} onChange={(id, value) => { - switch (id) { - case "jobname": - setJobname(value); - break; - case "description": - setDescription(value); - break; - case "protected": - setProtected(value); - break; - default: - logger.debug(`${id}: ${value}`); - } - }} /> -
-
-
- - - - -
- - - - - ; + return + + +
+ + + + {messageBarContent && + + {messageBarContent.message} + + } + + + +
+ +
+
+ 0 ? Math.round((potentialSavings / totalCosts) * 10000) / 100 : 0}%)`, readonly: true }, + "compileCost": { label: nlsHPCC.CompileCost, type: "string", value: `${formatCost(workunit?.CompileCost)}`, readonly: true }, + "executeCost": { label: nlsHPCC.ExecuteCost, type: "string", value: `${formatCost(workunit?.ExecuteCost)}`, readonly: true }, + "fileAccessCost": { label: nlsHPCC.FileAccessCost, type: "string", value: `${formatCost(workunit?.FileAccessCost)}`, readonly: true }, + "protected": { label: nlsHPCC.Protected, type: "checkbox", value: _protected }, + "cluster": { label: nlsHPCC.Cluster, type: "string", value: workunit?.Cluster, readonly: true }, + "totalClusterTime": { label: nlsHPCC.TotalClusterTime, type: "string", value: workunit?.TotalClusterTime ? workunit?.TotalClusterTime : "0.00", readonly: true }, + "abortedBy": { label: nlsHPCC.AbortedBy, type: "string", value: workunit?.AbortBy, readonly: true }, + "abortedTime": { label: nlsHPCC.AbortedTime, type: "string", value: workunit?.AbortTime, readonly: true }, + "ServiceNamesCustom": { label: nlsHPCC.Services, type: "string", value: serviceNames, readonly: true, multiline: true }, + }} onChange={(id, value) => { + switch (id) { + case "jobname": + setJobname(value); + break; + case "description": + setDescription(value); + break; + case "protected": + setProtected(value); + break; + default: + logger.debug(`${id}: ${value}`); + } + }} /> +
+
+
+ + + + +
+ + + + + } + />; }; diff --git a/esp/src/src-react/components/forms/ZAPDialog.tsx b/esp/src/src-react/components/forms/ZAPDialog.tsx index 01bb99a0482..1cd42fc86c1 100644 --- a/esp/src/src-react/components/forms/ZAPDialog.tsx +++ b/esp/src/src-react/components/forms/ZAPDialog.tsx @@ -107,7 +107,7 @@ const defaultValues: ZAPDialogValues = { StartDate: "", EndDate: "", }, - RelativeTimeRangeBuffer: "", + RelativeTimeRangeBuffer: "43200", LineLimit: "10000", LineStartFrom: "0", SelectColumnMode: ColumnMode.DEFAULT, @@ -198,8 +198,11 @@ export const ZAPDialog: React.FunctionComponent = ({ if (key === "AbsoluteTimeRange") { const startDate = logFilter.AbsoluteTimeRange.StartDate ? new Date(logFilter.AbsoluteTimeRange.StartDate).toISOString() : ""; const endDate = logFilter.AbsoluteTimeRange.EndDate ? new Date(logFilter.AbsoluteTimeRange.EndDate).toISOString() : ""; - formData.append("LogFilter_AbsoluteTimeRange_StartDate", startDate); - formData.append("LogFilter_AbsoluteTimeRange_EndDate", endDate); + if (startDate && endDate) { + formData.append("LogFilter_AbsoluteTimeRange_StartDate", startDate); + formData.append("LogFilter_AbsoluteTimeRange_EndDate", endDate); + delete logFilter.RelativeTimeRangeBuffer; + } } else { formData.append(`LogFilter_${key}`, logFilter[key]); } diff --git a/esp/src/src-react/routes.tsx b/esp/src/src-react/routes.tsx index 6aee9badfc4..112e77ef54e 100644 --- a/esp/src/src-react/routes.tsx +++ b/esp/src/src-react/routes.tsx @@ -25,7 +25,7 @@ const workunitsChildren: Route[] = [ }, { path: "/:Wuid", action: (ctx, params) => import("./components/WorkunitDetails").then(_ => { - return <_.WorkunitDetails wuid={params.Wuid as string} parentUrl={params.parentUrl as string} />; + return <_.WorkunitDetails wuid={params.Wuid as string} parentUrl={params.parentUrl as string} queryParams={{ summary: parseSearch(ctx.search) as any }} />; }) }, { diff --git a/esp/src/src/ESPLog.ts b/esp/src/src/ESPLog.ts index 32a195cff87..5f0c09ed115 100644 --- a/esp/src/src/ESPLog.ts +++ b/esp/src/src/ESPLog.ts @@ -1,4 +1,4 @@ -import { LogaccessService, LogLine, GetLogsExRequest, WsLogaccess } from "@hpcc-js/comms"; +import { LogaccessService, LogLine, GetLogsExRequest, WsLogaccess, Exceptions } from "@hpcc-js/comms"; import { scopedLogger } from "@hpcc-js/util"; import * as Observable from "dojo/store/Observable"; import { Paged } from "./store/Paged"; @@ -8,8 +8,12 @@ const logger = scopedLogger("src/ESPLog.ts"); export const service = new LogaccessService({ baseUrl: "" }); -let g_logAccessInfo: Promise; -export function GetLogAccessInfo(): Promise { +function isExceptionResponse(response: WsLogaccess.GetLogAccessInfoResponse | { Exceptions?: Exceptions }): response is { Exceptions?: Exceptions } { + return (response as { Exceptions?: Exceptions }).Exceptions !== undefined; +} + +let g_logAccessInfo: Promise; +export function GetLogAccessInfo(): Promise { if (!g_logAccessInfo) { g_logAccessInfo = service.GetLogAccessInfo({}); } @@ -41,8 +45,16 @@ export function CreateLogsQueryStore(): LogsQuerySto export function hasLogAccess(): Promise { return GetLogAccessInfo().then(response => { - return response.RemoteLogManagerConnectionString !== null || response.RemoteLogManagerType !== null; + if (isExceptionResponse(response)) { + const err = response.Exceptions.Exception[0].Message; + logger.error(err); + return false; + } else { + response = response as WsLogaccess.GetLogAccessInfoResponse; + return response?.RemoteLogManagerConnectionString !== null || response?.RemoteLogManagerType !== null; + } }).catch(e => { + logger.error(e); return false; }); }