From 14fa48fdd02d3b1dd30fe452b0bc8dcccb2653e5 Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Tue, 28 Jul 2026 13:42:33 +0200 Subject: [PATCH] app/vmui: add back action for hits chart time range (#1535) --- .../Chart/BarHitsChart/BarHitsChart.tsx | 59 ++++++++++++++++++- .../BarHitsOptions/BarHitsOptions.tsx | 41 ++++++++++++- .../BarHitsChart/BarHitsOptions/style.scss | 7 +++ .../vmui/src/components/Main/Icons/index.tsx | 9 +++ docs/victorialogs/CHANGELOG.md | 1 + 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx index b98092ca05..1fca597165 100644 --- a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo, useState } from "preact/compat"; +import { FC, useEffect, useMemo, useRef, useState } from "preact/compat"; import "./style.scss"; import "uplot/dist/uPlot.min.css"; import { AlignedData } from "uplot"; @@ -11,6 +11,7 @@ import { calculateTotalHits } from "../../../utils/logs"; import BarHitsStats from "./BarHitsStats/BarHitsStats"; import { HitsChartAlert } from "../../../pages/QueryPage/HitsPanel/hooks/useHitsChartAlert"; import Alert from "../../Main/Alert/Alert"; +import { timeParamsToDateRange } from "../../../utils/time"; interface Props { logHits: LogHits[]; @@ -45,6 +46,58 @@ const BarHitsChart: FC = ({ const isHitsMode = graphOptions.queryMode === GRAPH_QUERY_MODE.hits; const totalHits = useMemo(() => calculateTotalHits(logHits), [logHits]); + const currentPeriodRef = useRef(period); + const chartPeriodChangeRef = useRef(false); + const periodChangeTimeoutRef = useRef>(); + const periodBeforeInteractionRef = useRef(); + const [prevPeriod, setPrevPeriod] = useState(); + + const handleChangePeriod = (nextPeriod: TimePeriod) => { + chartPeriodChangeRef.current = true; + + if (periodChangeTimeoutRef.current === undefined) { + periodBeforeInteractionRef.current = currentPeriodRef.current; + } + + clearTimeout(periodChangeTimeoutRef.current); + + periodChangeTimeoutRef.current = setTimeout(() => { + setPrevPeriod(periodBeforeInteractionRef.current); + periodBeforeInteractionRef.current = undefined; + periodChangeTimeoutRef.current = undefined; + chartPeriodChangeRef.current = false; + }, 500); + + setPeriod(nextPeriod); + }; + + + const resetPeriodInteraction = () => { + clearTimeout(periodChangeTimeoutRef.current); + periodChangeTimeoutRef.current = undefined; + periodBeforeInteractionRef.current = undefined; + setPrevPeriod(undefined); + }; + + const handleRevertPeriod = () => { + if (!prevPeriod) return; + + setPeriod(timeParamsToDateRange(prevPeriod)); + resetPeriodInteraction(); + }; + + useEffect(() => { + currentPeriodRef.current = period; + + if (chartPeriodChangeRef.current) { + return; + } + + resetPeriodInteraction(); + }, [period]); + + useEffect(() => resetPeriodInteraction, []); + return (
@@ -60,6 +113,8 @@ const BarHitsChart: FC = ({ query={query} isHitsMode={isHitsMode} isOverview={isOverview} + prevPeriod={prevPeriod} + onRevertPeriod={handleRevertPeriod} onChange={setGraphOptions} />
@@ -76,7 +131,7 @@ const BarHitsChart: FC = ({ totalHits={totalHits} data={_data} period={period} - setPeriod={setPeriod} + setPeriod={handleChangePeriod} graphOptions={graphOptions} /> )} diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/BarHitsOptions.tsx b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/BarHitsOptions.tsx index 13f2ec6eb4..648ecab0b1 100644 --- a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/BarHitsOptions.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/BarHitsOptions.tsx @@ -5,7 +5,13 @@ import "./style.scss"; import useStateSearchParams from "../../../../hooks/useStateSearchParams"; import { useSearchParams } from "react-router-dom"; import Button from "../../../Main/Button/Button"; -import { KeyboardIcon, MoreIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons"; +import { + ArrowBackIcon, + KeyboardIcon, + MoreIcon, + VisibilityIcon, + VisibilityOffIcon +} from "../../../Main/Icons"; import Tooltip from "../../../Main/Tooltip/Tooltip"; import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys"; import { useCallback } from "react"; @@ -19,17 +25,23 @@ import { useHitsChartConfig } from "../../../../pages/QueryPage/HitsPanel/hooks/ import { useExtraFilters } from "../../../ExtraFilters/hooks/useExtraFilters"; import { useFetchFieldNames } from "../../../../pages/OverviewPage/hooks/useFetchFieldNames"; import { getDefaultIntervalOption, getIntervalOptions } from "../../../../utils/intervals"; +import { nanosToIsoString, vmDate } from "../../../../utils/time"; import { useTimePeriod } from "../../../../pages/QueryPage/hooks/useTimePeriod"; import usePrevious from "../../../../hooks/usePrevious"; +import { TimeParams } from "../../../../types"; +import { DATE_TIME_FORMAT } from "../../../../constants/date"; +import { useTimeState } from "../../../../state/time/TimeStateContext"; interface Props { query?: string; isHitsMode?: boolean; isOverview?: boolean; + prevPeriod?: TimeParams; + onRevertPeriod: () => void onChange: (options: GraphOptions) => void; } -const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, onChange }) => { +const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, prevPeriod, onRevertPeriod, onChange }) => { const { isMobile } = useDeviceDetect(); const { value: openList, @@ -40,6 +52,7 @@ const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, onChange }) const [searchParams, setSearchParams] = useSearchParams(); const { topHits, groupFieldHits, step } = useHitsChartConfig(); + const { timezone } = useTimeState(); const { extraParams } = useExtraFilters(); const { period: { start, end } } = useTimePeriod(); @@ -55,6 +68,14 @@ const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, onChange }) const [cumulative, setCumulative] = useStateSearchParams(false, "cumulative"); const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart"); + const prevPeriodFormatted = useMemo(() => { + if (!prevPeriod) return; + + const startIso = nanosToIsoString(prevPeriod.start); + const endIso = nanosToIsoString(prevPeriod.end); + return `${vmDate(startIso).tz().format(DATE_TIME_FORMAT)} - ${vmDate(endIso).tz().format(DATE_TIME_FORMAT)}`; + }, [prevPeriod, timezone]); + const options: GraphOptions = useMemo(() => ({ graphStyle: GRAPH_STYLES.BAR, queryMode, @@ -180,6 +201,20 @@ const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, onChange }) />
)} + + {prevPeriod && ( + +
+ +
+
+ )}
@@ -213,7 +248,7 @@ const BarHitsOptions: FC = ({ query, isHitsMode, isOverview, onChange }) className={classNames({ "vm-bar-hits-options": true, "vm-bar-hits-options_mobile": isMobile, - "vm-bar-hits-options_hidden": hideChart, + "vm-bar-hits-options_hidden": hideChart, })} > {!isMobile && !hideChart && ( diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/style.scss b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/style.scss index c507de25db..8598a73d60 100644 --- a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/style.scss +++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/style.scss @@ -29,6 +29,13 @@ &_switch { padding-right: $padding-global; } + + &_timerange { + gap: calc($padding-small / 4); + padding-inline: calc($padding-small / 2) $padding-small; + grid-template-columns: 16px 1fr; + min-height: 24px; + } } &_mobile { diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx index bb6db3d37c..fc3f3a6cdf 100644 --- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx @@ -113,6 +113,15 @@ export const ArrowDropDownIcon = () => ( ); +export const ArrowBackIcon = () => ( + + + +); + export const ClockIcon = () => (