Skip to content

Commit

Permalink
vmui: fix URL params handling for navigation (#6284)
Browse files Browse the repository at this point in the history
This PR fixes the handling of URL parameters to ensure correct browser
navigation using the back and forward buttons.

#6126

#5516 (comment)
(cherry picked from commit f14497f)
  • Loading branch information
Loori-R authored and hagen1778 committed May 20, 2024
1 parent 97c3c94 commit 33eaa18
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
}
}, [stateQuery, awaitStateQuery]);

useEffect(() => {
setStateQuery(query || []);
}, [query]);

return <div
className={classNames({
"vm-query-configurator": true,
Expand Down
133 changes: 111 additions & 22 deletions app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,127 @@
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();
const { displayType } = useCustomPanelState();
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 ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
if ((searchParams.get(`${group}.expr`) !== q) && q) {
newSearchParams.set(`${group}.expr`, q);
}

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]);

useEffect(() => {
const timer = setTimeout(setterSearchParams, 200);
return () => clearTimeout(timer);
}, [setterSearchParams]);

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);

setSearchParams(compactObject(params) as Record<string, string>);
};
return () => clearTimeout(timer);
}, [searchParams, isPopstate]);

useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
useEffect(setSearchParamsFromState, []);
useEventListener("popstate", () => {
setIsPopstate(true);
});
};
4 changes: 0 additions & 4 deletions app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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]);
Expand Down
10 changes: 7 additions & 3 deletions app/vmui/packages/vmui/src/state/customPanel/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 21 additions & 9 deletions app/vmui/packages/vmui/src/state/time/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions app/vmui/packages/vmui/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 33eaa18

Please sign in to comment.