vmui: fix URL params handling for navigation (#6284)

This PR fixes the handling of URL parameters to ensure correct browser
navigation using the back and forward buttons.

#6126

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5516#issuecomment-1867507232
(cherry picked from commit f14497f1cd)
This commit is contained in:
Yury Molodov 2024-05-20 14:39:08 +02:00 committed by hagen1778
parent 97c3c946a7
commit 33eaa18c14
No known key found for this signature in database
GPG Key ID: 3BF75F3741CA9640
7 changed files with 157 additions and 37 deletions

View File

@ -178,6 +178,10 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
}
}, [stateQuery, awaitStateQuery]);
useEffect(() => {
setStateQuery(query || []);
}, [query]);
return <div
className={classNames({
"vm-query-configurator": true,

View File

@ -1,12 +1,18 @@
import { useEffect } from "react";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useAppState } from "../../../state/common/StateContext";
import { useQueryState } from "../../../state/query/QueryStateContext";
import { useEffect, useState } from "react";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import { displayTypeTabs } from "../DisplayTypeSwitch";
import { compactObject } from "../../../utils/object";
import { useGraphState } from "../../../state/graph/GraphStateContext";
import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
import { useSearchParams } from "react-router-dom";
import { useCallback } from "preact/compat";
import { getInitialDisplayType } from "../../../state/customPanel/reducer";
import { getInitialTimeState } from "../../../state/time/reducer";
import useEventListener from "../../../hooks/useEventListener";
import { getQueryArray } from "../../../utils/query-string";
import { arrayEquals } from "../../../utils/array";
import { isEqualURLSearchParams } from "../../../utils/url";
export const useSetQueryParams = () => {
const { tenantId } = useAppState();
@ -14,25 +20,108 @@ export const useSetQueryParams = () => {
const { query } = useQueryState();
const { duration, relativeTime, period: { date, step } } = useTimeState();
const { customStep } = useGraphState();
const [, setSearchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const dispatch = useAppDispatch();
const timeDispatch = useTimeDispatch();
const graphDispatch = useGraphDispatch();
const queryDispatch = useQueryDispatch();
const customPanelDispatch = useCustomPanelDispatch();
const [isPopstate, setIsPopstate] = useState(false);
const setterSearchParams = useCallback(() => {
if (isPopstate) {
// After the popstate event, the states synchronizes with the searchParams,
// so there's no need to refresh the searchParams again.
setIsPopstate(false);
return;
}
const newSearchParams = new URLSearchParams(searchParams);
const setSearchParamsFromState = () => {
const params: Record<string, unknown> = {};
query.forEach((q, i) => {
const group = `g${i}`;
params[`${group}.expr`] = q;
params[`${group}.range_input`] = duration;
params[`${group}.end_input`] = date;
params[`${group}.tab`] = displayTypeTabs.find(t => t.value === displayType)?.prometheusCode || 0;
params[`${group}.relative_time`] = relativeTime;
params[`${group}.tenantID`] = tenantId;
if ((searchParams.get(`${group}.expr`) !== q) && q) {
newSearchParams.set(`${group}.expr`, q);
}
if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
if (searchParams.get(`${group}.range_input`) !== duration) {
newSearchParams.set(`${group}.range_input`, duration);
}
if (searchParams.get(`${group}.end_input`) !== date) {
newSearchParams.set(`${group}.end_input`, date);
}
if (searchParams.get(`${group}.relative_time`) !== relativeTime) {
newSearchParams.set(`${group}.relative_time`, relativeTime || "none");
}
const stepFromUrl = searchParams.get(`${group}.step_input`) || step;
if (stepFromUrl && (stepFromUrl !== customStep)) {
newSearchParams.set(`${group}.step_input`, customStep);
}
const displayTypeCode = `${displayTypeTabs.find(t => t.value === displayType)?.prometheusCode || 0}`;
if (searchParams.get(`${group}.tab`) !== displayTypeCode) {
newSearchParams.set(`${group}.tab`, `${displayTypeCode}`);
}
if (searchParams.get(`${group}.tenantID`) !== tenantId && tenantId) {
newSearchParams.set(`${group}.tenantID`, tenantId);
}
});
if (isEqualURLSearchParams(newSearchParams, searchParams) || !newSearchParams.size) return;
setSearchParams(newSearchParams);
}, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
setSearchParams(compactObject(params) as Record<string, string>);
};
useEffect(() => {
const timer = setTimeout(setterSearchParams, 200);
return () => clearTimeout(timer);
}, [setterSearchParams]);
useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
useEffect(setSearchParamsFromState, []);
useEffect(() => {
// Synchronize the states with searchParams only after the popstate event.
if (!isPopstate) return;
const timeFromUrl = getInitialTimeState();
const isDurationDifferent = (timeFromUrl.duration !== duration);
const isRelativeTimeDifferent = timeFromUrl.relativeTime !== relativeTime;
const isDateDifferent = timeFromUrl.relativeTime === "none" && timeFromUrl.period.date !== date;
const someNotEqual = isDurationDifferent || isRelativeTimeDifferent || isDateDifferent;
if (someNotEqual) {
timeDispatch({ type: "SET_TIME_STATE", payload: timeFromUrl });
}
const displayTypeFromUrl = getInitialDisplayType();
if (displayTypeFromUrl !== displayType) {
customPanelDispatch({ type: "SET_DISPLAY_TYPE", payload: displayTypeFromUrl });
}
const tenantIdFromUrl = searchParams.get("g0.tenantID") || "";
if (tenantIdFromUrl !== tenantId) {
dispatch({ type: "SET_TENANT_ID", payload: tenantIdFromUrl });
}
const queryFromUrl = getQueryArray();
if (!arrayEquals(queryFromUrl, query)) {
queryDispatch({ type: "SET_QUERY", payload: queryFromUrl });
timeDispatch({ type: "RUN_QUERY" });
}
// Timer prevents customStep reset on time range change.
const timer = setTimeout(() => {
const customStepFromUrl = searchParams.get("g0.step_input") || step;
if (customStepFromUrl && customStepFromUrl !== customStep) {
graphDispatch({ type: "SET_CUSTOM_STEP", payload: customStepFromUrl });
}
}, 50);
return () => clearTimeout(timer);
}, [searchParams, isPopstate]);
useEventListener("popstate", () => {
setIsPopstate(true);
});
};

View File

@ -12,7 +12,6 @@ import Alert from "../../components/Main/Alert/Alert";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import InstantQueryTip from "./InstantQueryTip/InstantQueryTip";
import useEventListener from "../../hooks/useEventListener";
import { useRef } from "react";
import CustomPanelTraces from "./CustomPanelTraces/CustomPanelTraces";
import WarningLimitSeries from "./WarningLimitSeries/WarningLimitSeries";
@ -65,9 +64,6 @@ const CustomPanel: FC = () => {
setHideError(false);
};
const handleChangePopstate = () => window.location.reload();
useEventListener("popstate", handleChangePopstate);
useEffect(() => {
graphDispatch({ type: "SET_IS_HISTOGRAM", payload: isHistogram });
}, [graphData]);

View File

@ -19,12 +19,16 @@ export type CustomPanelAction =
| { type: "TOGGLE_QUERY_TRACING" }
| { type: "TOGGLE_TABLE_COMPACT" }
const queryTab = getQueryStringValue("g0.tab", 0) as string;
const displayType = displayTypeTabs.find(t => t.prometheusCode === +queryTab || t.value === queryTab);
export const getInitialDisplayType = () => {
const queryTab = getQueryStringValue("g0.tab", 0) as string;
const displayType = displayTypeTabs.find(t => t.prometheusCode === +queryTab || t.value === queryTab);
return displayType?.value || DisplayType.chart;
};
const limitsStorage = getFromStorage("SERIES_LIMITS") as string;
export const initialCustomPanelState: CustomPanelState = {
displayType: (displayType?.value || DisplayType.chart),
displayType: getInitialDisplayType(),
nocache: false,
isTracingEnabled: false,
seriesLimits: limitsStorage ? JSON.parse(limitsStorage) : DEFAULT_MAX_SERIES,

View File

@ -21,6 +21,7 @@ export interface TimeState {
}
export type TimeAction =
| { type: "SET_TIME_STATE", payload: { duration: string, period: TimeParams, relativeTime?: string; } }
| { type: "SET_DURATION", payload: string }
| { type: "SET_RELATIVE_TIME", payload: {id: string, duration: string, until: Date} }
| { type: "SET_PERIOD", payload: TimePeriod }
@ -32,24 +33,35 @@ export type TimeAction =
const timezone = getFromStorage("TIMEZONE") as string || getBrowserTimezone().region;
setTimezone(timezone);
const defaultDuration = getQueryStringValue("g0.range_input") as string;
export const getInitialTimeState = () => {
const defaultDuration = getQueryStringValue("g0.range_input") as string;
const { duration, endInput, relativeTimeId } = getRelativeTime({
defaultDuration: defaultDuration || "1h",
defaultEndInput: formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as string),
relativeTimeId: defaultDuration ? getQueryStringValue("g0.relative_time", "none") as string : undefined
});
const { duration, endInput, relativeTimeId } = getRelativeTime({
defaultDuration: defaultDuration || "1h",
defaultEndInput: formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as string),
relativeTimeId: defaultDuration ? getQueryStringValue("g0.relative_time", "none") as string : undefined
});
return {
duration,
period: getTimeperiodForDuration(duration, endInput),
relativeTime: relativeTimeId,
};
};
export const initialTimeState: TimeState = {
duration,
period: getTimeperiodForDuration(duration, endInput),
relativeTime: relativeTimeId,
...getInitialTimeState(),
timezone,
};
export function reducer(state: TimeState, action: TimeAction): TimeState {
switch (action.type) {
case "SET_TIME_STATE":
return {
...state,
...action.payload
};
case "SET_DURATION":
return {
...state,

View File

@ -11,3 +11,17 @@ export const isValidHttpUrl = (str: string): boolean => {
};
export const removeTrailingSlash = (url: string) => url.replace(/\/$/, "");
export const isEqualURLSearchParams = (params1: URLSearchParams, params2: URLSearchParams): boolean => {
if (Array.from(params1.entries()).length !== Array.from(params2.entries()).length) {
return false;
}
for (const [key, value] of params1) {
if (params2.get(key) !== value) {
return false;
}
}
return true;
};

View File

@ -50,6 +50,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix bug that prevents the first query trace from expanding on click event. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6186). The issue was introduced in [v1.100.0](https://docs.victoriametrics.com/changelog/#v11000) release.
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix calendar display when `UTC+00:00` timezone is set. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6239).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): remove redundant requests on the `Explore Cardinality` page. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6240).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix handling of URL params for browser history navigation (back and forward buttons). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6126) and [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5516#issuecomment-1867507232).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent/): prevent potential panic during [stream aggregation](https://docs.victoriametrics.com/stream-aggregation.html) if more than one `--remoteWrite.streamAggr.dedupInterval` is configured. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6205).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent/): skip empty data blocks before sending to the remote write destination. Thanks to @viperstars for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6241).
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): set correct suffix `<output>_prometheus` for aggregation outputs [increase_prometheus](https://docs.victoriametrics.com/stream-aggregation/#increase_prometheus) and [total_prometheus](https://docs.victoriametrics.com/stream-aggregation/#total_prometheus). Before, outputs `total` and `total_prometheus` or `increase` and `increase_prometheus` had the same suffix.