diff --git a/esp/src/package-lock.json b/esp/src/package-lock.json index ecb1390c775..3391efbb526 100644 --- a/esp/src/package-lock.json +++ b/esp/src/package-lock.json @@ -51,6 +51,7 @@ "react-hook-form": "7.51.2", "react-hot-toast": "2.4.1", "react-reflex": "4.2.6", + "react-singleton-hook": "3.4.0", "react-sizeme": "3.0.2", "universal-router": "9.2.0", "xstyle": "0.3.3" @@ -9777,6 +9778,23 @@ "react-dom": ">0.13.0" } }, + "node_modules/react-singleton-hook": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-3.4.0.tgz", + "integrity": "sha512-eQEpyacGAaRejmWUizUdNNQFn5AO0iaKRSl1jxgC0FQadVY/I1WFuPrYiutglPzO9s8yEbIh95UXVJQel4d7HQ==", + "license": "MIT", + "peerDependencies": { + "react": "15 - 18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-sizeme": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-3.0.2.tgz", diff --git a/esp/src/package.json b/esp/src/package.json index e9d7224d6f4..de3511485ec 100644 --- a/esp/src/package.json +++ b/esp/src/package.json @@ -77,6 +77,7 @@ "react-hook-form": "7.51.2", "react-hot-toast": "2.4.1", "react-reflex": "4.2.6", + "react-singleton-hook": "3.4.0", "react-sizeme": "3.0.2", "universal-router": "9.2.0", "xstyle": "0.3.3" diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index 40413d012c7..95d16528912 100644 --- a/esp/src/src-react/components/Metrics.tsx +++ b/esp/src/src-react/components/Metrics.tsx @@ -11,7 +11,7 @@ import { scopedLogger } from "@hpcc-js/util"; import nlsHPCC from "src/nlsHPCC"; import { WUTimelineNoFetch } from "src/Timings"; import * as Utility from "src/Utility"; -import { FetchStatus, useMetricsOptions, useWUQueryMetrics, MetricsOptions as MetricsOptionsT } from "../hooks/metrics"; +import { FetchStatus, useMetricsViews, useWUQueryMetrics } from "../hooks/metrics"; import { HolyGrail } from "../layouts/HolyGrail"; import { AutosizeComponent, AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter"; import { DockPanel, DockPanelItem, ResetableDockPanel } from "../layouts/DockPanel"; @@ -89,10 +89,10 @@ class TableEx extends Table { } _rawDataMap: { [id: number]: string } = {}; - metrics(metrics: any[], options: MetricsOptionsT, scopeFilter: string, matchCase: boolean): this { + metrics(metrics: any[], scopeTypes: string[], properties: string[], scopeFilter: string, matchCase: boolean): this { this .columns(["##"]) // Reset hash to force recalculation of default widths - .columns(["##", nlsHPCC.Type, "StdDevs", nlsHPCC.Scope, ...options.properties, "__StdDevs"]) + .columns(["##", nlsHPCC.Type, "StdDevs", nlsHPCC.Scope, ...properties, "__StdDevs"]) .columnFormats([ new ColumnFormatEx() .column("StdDevs") @@ -106,18 +106,18 @@ class TableEx extends Table { .data(metrics .filter(m => this.scopeFilterFunc(m, scopeFilter, matchCase)) .filter(row => { - return options.scopeTypes.indexOf(row.type) >= 0; + return scopeTypes.indexOf(row.type) >= 0; }).map((row, idx) => { if (idx === 0) { this._rawDataMap = { 0: "##", 1: "type", 2: "__StdDevs", 3: "name" }; - options.properties.forEach((p, idx2) => { + properties.forEach((p, idx2) => { this._rawDataMap[4 + idx2] = p; }); } row.__hpcc_id = row.name; - return [idx, row.type, row.__StdDevs === 0 ? undefined : row.__StdDevs, row.name, ...options.properties.map(p => { + return [idx, row.type, row.__StdDevs === 0 ? undefined : row.__StdDevs, row.name, ...properties.map(p => { return row.__groupedProps[p]?.Value ?? row.__groupedProps[p]?.Max ?? row.__groupedProps[p]?.Avg ?? @@ -157,6 +157,7 @@ class TableEx extends Table { } type SelectedMetricsSource = "" | "scopesTable" | "scopesSqlTable" | "metricGraphWidget" | "hotspot" | "reset"; +const TIMELINE_FIXEDHEIGHT = 152; interface MetricsProps { wuid: string; @@ -178,10 +179,9 @@ export const Metrics: React.FunctionComponent = ({ const [selectedMetrics, setSelectedMetrics] = React.useState([]); const [selectedMetricsPtr, setSelectedMetricsPtr] = React.useState(-1); const [metrics, columns, _activities, _properties, _measures, _scopeTypes, fetchStatus, refresh] = useWUQueryMetrics(wuid, querySet, queryId); + const { viewIds, viewId, setViewId, view, updateView } = useMetricsViews(); const [showMetricOptions, setShowMetricOptions] = React.useState(false); - const [options, setOptions, saveOptions] = useMetricsOptions(); const [dockpanel, setDockpanel] = React.useState(); - const [showTimeline, setShowTimeline] = React.useState(true); const [trackSelection, setTrackSelection] = React.useState(true); const [fullscreen, setFullscreen] = React.useState(false); const [hotspots, setHotspots] = React.useState(""); @@ -240,11 +240,14 @@ export const Metrics: React.FunctionComponent = ({ }, [parentUrl, timeline]); React.useEffect(() => { - timeline - .scopes(metrics) - .lazyRender() - ; - }, [metrics, timeline]); + if (view.showTimeline) { + timeline + .scopes(metrics) + .height(TIMELINE_FIXEDHEIGHT) + .lazyRender() + ; + } + }, [metrics, timeline, view.showTimeline]); // Scopes Table --- const onChangeScopeFilter = React.useCallback((event: React.FormEvent, newValue?: string) => { @@ -258,7 +261,7 @@ export const Metrics: React.FunctionComponent = ({ const scopesTable = useConst(() => new TableEx() .multiSelect(true) - .metrics([], options, scopeFilter, matchCase) + .metrics([], view.scopeTypes, view.properties, scopeFilter, matchCase) .sortable(true) ); @@ -274,13 +277,13 @@ export const Metrics: React.FunctionComponent = ({ React.useEffect(() => { scopesTable - .metrics(metrics, options, scopeFilter, matchCase) + .metrics(metrics, view.scopeTypes, view.properties, scopeFilter, matchCase) .lazyRender() ; - }, [matchCase, metrics, options, scopeFilter, scopesTable]); + }, [matchCase, metrics, scopeFilter, scopesTable, view.properties, view.scopeTypes]); const updateScopesTable = React.useCallback((selection: IScope[]) => { - if (scopesTable?.renderCount() > 0) { + if (scopesTable?.renderCount() > 0 && selectedMetricsSource !== "scopesTable") { scopesTable.selection([]); if (selection.length) { const selRows = scopesTable.data().filter(row => { @@ -291,7 +294,7 @@ export const Metrics: React.FunctionComponent = ({ }); } } - }, [scopesTable]); + }, [scopesTable, selectedMetricsSource]); // Graph --- const metricGraph = useConst(() => new MetricGraph()); @@ -490,9 +493,9 @@ export const Metrics: React.FunctionComponent = ({ }, [crossTabTable]); React.useEffect(() => { - const dot = metricGraph.graphTpl(selectedLineage ? [selectedLineage] : [], options); + const dot = metricGraph.graphTpl(selectedLineage ? [selectedLineage] : [], view); setDot(dot); - }, [metricGraph, options, selectedLineage]); + }, [metricGraph, view, selectedLineage]); React.useEffect(() => { let cancelled = false; @@ -530,13 +533,14 @@ export const Metrics: React.FunctionComponent = ({ React.useEffect(() => { // Update layout prior to unmount --- - if (dockpanel && options && saveOptions && setOptions) { + if (dockpanel && updateView) { return () => { - setOptions({ ...options, layout: dockpanel.getLayout() }); - saveOptions(); + if (dockpanel && updateView) { + updateView({ layout: dockpanel.getLayout() }); + } }; } - }, [dockpanel, options, saveOptions, setOptions]); + }, [dockpanel, updateView]); // Command Bar --- const buttons = React.useMemo((): ICommandBarItemProps[] => [ @@ -556,19 +560,30 @@ export const Metrics: React.FunctionComponent = ({ }, { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => }, { - key: "timeline", text: nlsHPCC.Timeline, canCheck: true, checked: showTimeline, iconProps: { iconName: "TimelineProgress" }, + key: "views", text: viewId, iconProps: { iconName: "View" }, + subMenuProps: { + items: viewIds.map(v => ({ + key: v, text: v, onClick: () => { + updateView({ layout: dockpanel.getLayout() }); + setViewId(v); + } + })) + }, + }, + { + key: "timeline", text: nlsHPCC.Timeline, canCheck: true, checked: view.showTimeline, iconProps: { iconName: "TimelineProgress" }, onClick: () => { - setShowTimeline(!showTimeline); + updateView({ showTimeline: !view.showTimeline }, true); } }, { key: "options", text: nlsHPCC.Options, iconProps: { iconName: "Settings" }, onClick: () => { - setOptions({ ...options, layout: dockpanel.layout() }); + updateView({ layout: dockpanel.getLayout() }); setShowMetricOptions(true); } } - ], [dockpanel, hotspots, onHotspot, options, refresh, setOptions, showTimeline, timeline]); + ], [dockpanel, hotspots, onHotspot, refresh, setViewId, timeline, updateView, view.showTimeline, viewId, viewIds]); const formatColumns = React.useMemo((): Utility.ColumnMap => { const copyColumns: Utility.ColumnMap = {}; @@ -610,7 +625,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) } @@ -618,23 +635,18 @@ export const Metrics: React.FunctionComponent = ({ const setShowMetricOptionsHook = React.useCallback((show: boolean) => { setShowMetricOptions(show); - scopesTable - .metrics(metrics, options, scopeFilter, matchCase) - .render(() => { - updateScopesTable(selectedMetrics); - }) - ; + }, []); - }, [matchCase, metrics, options, scopeFilter, scopesTable, selectedMetrics, updateScopesTable]); + console.log("View ID", viewId, view.scopeTypes); return -