From 620b3a516a0ce2b6ffe155b979f0c09b8bca2497 Mon Sep 17 00:00:00 2001 From: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:44:31 -0400 Subject: [PATCH 01/19] HPCC-32857 ECL Watch v9 Logs grid timestamp formatter fixes an issue with the ECL Watch v9 Logs viewer potentially having an uncaught JS exception if the logging engine is misconfigured and the value provided as the "timestamp" for a log message cannot be converted to a valid date Signed-off-by: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> --- esp/src/src-react/components/Logs.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esp/src/src-react/components/Logs.tsx b/esp/src/src-react/components/Logs.tsx index 3fcb095bdc2..98005a65873 100644 --- a/esp/src/src-react/components/Logs.tsx +++ b/esp/src/src-react/components/Logs.tsx @@ -137,7 +137,11 @@ export const Logs: React.FunctionComponent = ({ formatter: ts => { if (ts) { if (ts.indexOf(":") < 0) { - return timestampToDate(ts).toISOString(); + const date = timestampToDate(ts); + if (!isNaN(date.getTime())) { + return date.toISOString(); + } + return ts; } return formatDateString(ts); } From a610226f408495c9023095133b33dec9be60ba16 Mon Sep 17 00:00:00 2001 From: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:30:08 -0400 Subject: [PATCH 02/19] HPCC-32824 ECL Watch v9 show Open Telemetry ids on WU details adds the Open Telemetry trace and span ids to the WU details page, as well as a button to easily copy these values out of the UI Signed-off-by: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> --- .../eclwatch/img/opentelemetry-icon-color.svg | 1 + esp/src/src-react/components/Variables.tsx | 9 ++-- .../src-react/components/WorkunitDetails.tsx | 13 +++-- .../src-react/components/WorkunitSummary.tsx | 52 ++++++++++++++++++- esp/src/src/nls/hpcc.ts | 3 ++ 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 esp/src/eclwatch/img/opentelemetry-icon-color.svg diff --git a/esp/src/eclwatch/img/opentelemetry-icon-color.svg b/esp/src/eclwatch/img/opentelemetry-icon-color.svg new file mode 100644 index 00000000000..6633300d0ac --- /dev/null +++ b/esp/src/eclwatch/img/opentelemetry-icon-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esp/src/src-react/components/Variables.tsx b/esp/src/src-react/components/Variables.tsx index 6cb7f193df6..b3ca2e58453 100644 --- a/esp/src/src-react/components/Variables.tsx +++ b/esp/src/src-react/components/Variables.tsx @@ -2,24 +2,25 @@ import * as React from "react"; import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react"; import nlsHPCC from "src/nlsHPCC"; import { QuerySortItem } from "src/store/Store"; -import { useWorkunitVariables } from "../hooks/workunit"; +import { Variable } from "../hooks/workunit"; import { HolyGrail } from "../layouts/HolyGrail"; import { FluentGrid, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; import { ShortVerticalDivider } from "./Common"; interface VariablesProps { - wuid: string; + variables: Variable[]; + refreshData: () => void; sort?: QuerySortItem; } const defaultSort = { attribute: "Wuid", descending: true }; export const Variables: React.FunctionComponent = ({ - wuid, + variables, + refreshData, sort = defaultSort }) => { - const [variables, , , refreshData] = useWorkunitVariables(wuid); const [data, setData] = React.useState([]); const { selection, setSelection, diff --git a/esp/src/src-react/components/WorkunitDetails.tsx b/esp/src/src-react/components/WorkunitDetails.tsx index 47d632db043..0451dbd98b5 100644 --- a/esp/src/src-react/components/WorkunitDetails.tsx +++ b/esp/src/src-react/components/WorkunitDetails.tsx @@ -7,7 +7,7 @@ import nlsHPCC from "src/nlsHPCC"; import { hasLogAccess } from "src/ESPLog"; import { wuidToDate, wuidToTime } from "src/Utility"; import { emptyFilter, formatQuery } from "src/ESPWorkunit"; -import { useWorkunit } from "../hooks/workunit"; +import { Variable, useWorkunit, useWorkunitVariables } from "../hooks/workunit"; import { useDeepEffect } from "../hooks/deepHooks"; import { DojoAdapter } from "../layouts/DojoAdapter"; import { parseQuery, pushUrl } from "../util/history"; @@ -52,6 +52,8 @@ export const WorkunitDetails: React.FunctionComponent = ({ }) => { const [workunit] = useWorkunit(wuid, true); + const [variables, , , refreshVariables] = useWorkunitVariables(wuid); + const [otTraceParent, setOtTraceParent] = React.useState(""); const [logCount, setLogCount] = React.useState("*"); const [logsDisabled, setLogsDisabled] = React.useState(true); const [_nextPrev, setNextPrev] = useNextPrev(); @@ -64,6 +66,11 @@ export const WorkunitDetails: React.FunctionComponent = ({ return parseQuery("?" + parentUrlParts[1]); }, [parentUrl]); + React.useEffect(() => { + const traceInfo: Variable = variables.filter(v => v.Name === "ottraceparent")[0]; + setOtTraceParent(traceInfo?.Value ?? ""); + }, [variables]); + const nextWuid = React.useCallback((wuids: WsWorkunits.ECLWorkunit[]) => { let found = false; for (const wu of wuids) { @@ -177,10 +184,10 @@ export const WorkunitDetails: React.FunctionComponent = ({
- + - + {state?.outputs ? diff --git a/esp/src/src-react/components/WorkunitSummary.tsx b/esp/src/src-react/components/WorkunitSummary.tsx index a519dd1a36c..3bf1e0b4337 100644 --- a/esp/src/src-react/components/WorkunitSummary.tsx +++ b/esp/src/src-react/components/WorkunitSummary.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, MessageBar, MessageBarType, ScrollablePane, ScrollbarVisibility, Sticky, StickyPositionType } from "@fluentui/react"; +import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, mergeStyles, MessageBar, MessageBarType, registerIcons, ScrollablePane, ScrollbarVisibility, Sticky, StickyPositionType } from "@fluentui/react"; import { scopedLogger } from "@hpcc-js/util"; import nlsHPCC from "src/nlsHPCC"; import { WUStatus } from "src/react/index"; @@ -20,6 +20,35 @@ import { WorkunitPersona } from "./controls/StateIcon"; const logger = scopedLogger("../components/WorkunitDetails.tsx"); +registerIcons({ + icons: { + "open-telemetry": ( + // .../eclwatch/img/opentelemetry-icon-color.svg + + ) + } +}); + +const otIconStyle = mergeStyles({ + width: 16 +}); + +interface OtTraceSchema { + traceId: string; + spanId: string; +} + +const parseOtTraceParent = (parent: string = ""): OtTraceSchema => { + const retVal = { traceId: "", spanId: "" }; + const regex = /00\-([0-9a-z]+)\-([0-9a-z]+)\-01/; + const matches = parent.match(regex); + if (matches) { + retVal.traceId = matches[1] ?? ""; + retVal.spanId = matches[2] ?? ""; + } + return retVal; +}; + interface MessageBarContent { type: MessageBarType; message: string; @@ -27,11 +56,13 @@ interface MessageBarContent { interface WorkunitSummaryProps { wuid: string; + otTraceParent?: string; fullscreen?: boolean; } export const WorkunitSummary: React.FunctionComponent = ({ wuid, + otTraceParent = "", fullscreen = false }) => { @@ -39,6 +70,8 @@ export const WorkunitSummary: React.FunctionComponent = ({ const [exceptions, , refreshSavings] = useWorkunitExceptions(wuid); const [jobname, setJobname] = React.useState(""); const [description, setDescription] = React.useState(""); + const [otTraceId, setOtTraceId] = React.useState(""); + const [otSpanId, setOtSpanId] = React.useState(""); const [_protected, setProtected] = React.useState(false); const [showPublishForm, setShowPublishForm] = React.useState(false); const [showZapForm, setShowZapForm] = React.useState(false); @@ -60,6 +93,12 @@ export const WorkunitSummary: React.FunctionComponent = ({ setProtected(workunit?.Protected); }, [workunit?.Description, workunit?.Jobname, workunit?.Protected]); + React.useEffect(() => { + const otTrace = parseOtTraceParent(otTraceParent); + setOtTraceId(otTrace.traceId); + setOtSpanId(otTrace.spanId); + }, [otTraceParent]); + const canSave = workunit && ( jobname !== workunit.Jobname || description !== workunit.Description || @@ -98,6 +137,13 @@ export const WorkunitSummary: React.FunctionComponent = ({ navigator?.clipboard?.writeText(wuid); } }, + { + key: "copyOtel", text: nlsHPCC.CopyOpenTelemetry, iconProps: { iconName: "open-telemetry", className: otIconStyle }, + disabled: otTraceParent === "", + onClick: () => { + navigator?.clipboard?.writeText(JSON.stringify(parseOtTraceParent(otTraceParent))); + } + }, { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => }, { key: "save", text: nlsHPCC.Save, iconProps: { iconName: "Save" }, disabled: !canSave, @@ -172,7 +218,7 @@ export const WorkunitSummary: React.FunctionComponent = ({ key: "slaveLogs", text: nlsHPCC.SlaveLogs, disabled: !workunit?.ThorLogList, onClick: () => setShowThorSlaveLogs(true) }, - ], [_protected, canDelete, canDeschedule, canReschedule, canSave, description, jobname, refresh, refreshSavings, setShowDeleteConfirm, showMessageBar, workunit, wuid]); + ], [_protected, canDelete, canDeschedule, canReschedule, canSave, description, jobname, otTraceParent, refresh, refreshSavings, setShowDeleteConfirm, showMessageBar, workunit, wuid]); const rightButtons = React.useMemo((): ICommandBarItemProps[] => [ { @@ -222,6 +268,8 @@ export const WorkunitSummary: React.FunctionComponent = ({ Date: Wed, 30 Oct 2024 10:50:08 +0000 Subject: [PATCH 03/19] HPCC-32834 Refactor WU Details fullscreen Move fullscreen logic to WU Details to allow switching tabs in fullscreen mode Signed-off-by: Gordon Smith --- esp/src/src-react/components/FileDetails.tsx | 118 ++++++++------- esp/src/src-react/components/Helpers.tsx | 7 +- esp/src/src-react/components/Metrics.tsx | 28 +--- esp/src/src-react/components/QueryDetails.tsx | 76 +++++----- esp/src/src-react/components/Resources.tsx | 8 +- esp/src/src-react/components/Results.tsx | 9 +- .../src-react/components/WorkunitDetails.tsx | 134 +++++++++-------- .../src-react/components/WorkunitSummary.tsx | 15 +- esp/src/src-react/layouts/Fullscreen.tsx | 50 +++++++ esp/src/src-react/routes.tsx | 24 +-- esp/src/src-react/util/hashUrl.ts | 141 ++++++++++++++++++ esp/src/src-react/util/history.ts | 32 ++-- 12 files changed, 426 insertions(+), 216 deletions(-) create mode 100644 esp/src/src-react/layouts/Fullscreen.tsx create mode 100644 esp/src/src-react/util/hashUrl.ts diff --git a/esp/src/src-react/components/FileDetails.tsx b/esp/src/src-react/components/FileDetails.tsx index c936e617142..89c2fb8dc3c 100644 --- a/esp/src/src-react/components/FileDetails.tsx +++ b/esp/src/src-react/components/FileDetails.tsx @@ -3,9 +3,10 @@ import { WsDfu } from "@hpcc-js/comms"; import { SizeMe } from "react-sizeme"; import nlsHPCC from "src/nlsHPCC"; import { QuerySortItem } from "src/store/Store"; -import { FileParts } from "./FileParts"; import { useFile, useDefFile } from "../hooks/file"; -import { pushUrl, replaceUrl } from "../util/history"; +import { pushUrl, replaceUrl, updateFullscreen } from "../util/history"; +import { FullscreenFrame, FullscreenStack } from "../layouts/Fullscreen"; +import { FileParts } from "./FileParts"; import { FileBlooms } from "./FileBlooms"; import { FileHistory } from "./FileHistory"; import { ProtectedBy } from "./ProtectedBy"; @@ -28,6 +29,7 @@ interface FileDetailsProps { cluster?: string; logicalFile: string; tab?: string; + fullscreen?: boolean; sort?: { subfiles?: QuerySortItem, superfiles?: QuerySortItem, parts?: QuerySortItem, graphs?: QuerySortItem, history?: QuerySortItem, blooms?: QuerySortItem, protectby?: QuerySortItem }; queryParams?: { contents?: StringStringMap }; } @@ -36,6 +38,7 @@ export const FileDetails: React.FunctionComponent = ({ cluster, logicalFile, tab = "summary", + fullscreen = false, sort = {}, queryParams = {} }) => { @@ -50,7 +53,8 @@ export const FileDetails: React.FunctionComponent = ({ const onTabSelect = React.useCallback((tab: TabInfo) => { pushUrl(tab.__state ?? `/files/${cluster}/${logicalFile}/${tab.id}`); - }, [cluster, logicalFile]); + updateFullscreen(fullscreen); + }, [fullscreen, cluster, logicalFile]); const tabs = React.useMemo((): TabInfo[] => { return [{ @@ -103,56 +107,60 @@ export const FileDetails: React.FunctionComponent = ({ }]; }, [file]); - return {({ size }) => -
- - - {file?.ContentType === "key" - ? - : file?.isSuperfile - ? - : - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- }
; + return + {({ size }) => +
+ + + + + {file?.ContentType === "key" + ? + : file?.isSuperfile + ? + : + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ }
+
; }; diff --git a/esp/src/src-react/components/Helpers.tsx b/esp/src/src-react/components/Helpers.tsx index 73fed185eca..972a7453a4c 100644 --- a/esp/src/src-react/components/Helpers.tsx +++ b/esp/src/src-react/components/Helpers.tsx @@ -6,6 +6,8 @@ import { HelperRow, useWorkunitHelpers } from "../hooks/workunit"; import { HolyGrail } from "../layouts/HolyGrail"; import { FluentGrid, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; import { ShortVerticalDivider } from "./Common"; +import { SearchParams } from "../util/hashUrl"; +import { hashHistory } from "../util/history"; function canShowContent(type: string) { switch (type) { @@ -135,8 +137,11 @@ export const Helpers: React.FunctionComponent = ({ formatter: (Type, row) => { const target = getTarget(row.id, row); if (target) { + const searchParams = new SearchParams(hashHistory.location.search); + searchParams.param("mode", encodeURIComponent(target.sourceMode)); + searchParams.param("src", encodeURIComponent(target.url)); const linkText = Type.replace("Slave", "Worker") + (row?.Description ? " (" + row.Description + ")" : ""); - return {linkText}; + return {linkText}; } return Type; } diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index 3d1f91cc2bd..9b430ef6793 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 as _pushUrl } from "../util/history"; +import { pushUrl } from "../util/history"; import { debounce } from "../util/throttle"; import { ErrorBoundary } from "../util/errorBoundary"; import { ShortVerticalDivider } from "./Common"; @@ -45,7 +45,6 @@ interface MetricsProps { queryId?: string; parentUrl?: string; selection?: string; - fullscreen?: boolean; } export const Metrics: React.FunctionComponent = ({ @@ -53,8 +52,7 @@ export const Metrics: React.FunctionComponent = ({ querySet = "", queryId = "", parentUrl = `/workunits/${wuid}/metrics`, - selection, - fullscreen = false + selection }) => { const [_uiState, _setUIState] = React.useState({ ...defaultUIState }); const [selectedMetricsSource, setSelectedMetricsSource] = React.useState(""); @@ -97,20 +95,11 @@ export const Metrics: React.FunctionComponent = ({ }).catch(err => logger.error(err)); }, [wuid]); - const pushUrl = React.useCallback((selection?: string, fullscreen?: boolean) => { + const pushSelectionUrl = React.useCallback((selection: string) => { const selectionStr = selection?.length ? `/${selection}` : ""; - const fullscreenStr = fullscreen ? "?fullscreen" : ""; - _pushUrl(`${parentUrl}${selectionStr}${fullscreenStr}`); + pushUrl(`${parentUrl}${selectionStr}`); }, [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"); pushSelectionUrl(selection); @@ -532,18 +521,13 @@ export const Metrics: React.FunctionComponent = ({ }] } }, - { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => }, - { - key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" }, - onClick: () => pushFullscreenUrl(!fullscreen) - } - ], [dot, formatColumns, fullscreen, metrics, pushFullscreenUrl, wuid]); + ], [dot, formatColumns, metrics, wuid]); const setShowMetricOptionsHook = React.useCallback((show: boolean) => { setShowMetricOptions(show); }, []); - return