vmui: fix app routing issues (#4408)

The change focuses on rectifying inconsistencies in the navigation behavior of the application
and eliminating issues encountered when manually altering the URL.

The key updates include:
- Refactoring of the routing mechanism to handle all possible routes and their states.
- Enhancement of the React Router usage to ensure a smoother navigation experience.
- Handling application state when the URL is manually changed.
This commit is contained in:
Yury Molodov 2023-06-30 10:13:10 +02:00 committed by Aliaksandr Valialkin
parent 7eeb2d553f
commit 8c190ec8fb
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
22 changed files with 1285 additions and 1168 deletions

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@
"preact": "^10.7.1", "preact": "^10.7.1",
"qs": "^6.10.3", "qs": "^6.10.3",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.10.0",
"sass": "^1.56.0", "sass": "^1.56.0",
"typescript": "~4.6.2", "typescript": "~4.6.2",
"uplot": "^1.6.19", "uplot": "^1.6.19",

View file

@ -8,21 +8,22 @@ import { DATE_FORMAT } from "../../../constants/date";
import DatePicker from "../../Main/DatePicker/DatePicker"; import DatePicker from "../../Main/DatePicker/DatePicker";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
const CardinalityDatePicker: FC = () => { const CardinalityDatePicker: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const appModeEnable = getAppModeEnable(); const appModeEnable = getAppModeEnable();
const buttonRef = useRef<HTMLDivElement>(null); const buttonRef = useRef<HTMLDivElement>(null);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT); const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]); const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
const handleChangeDate = (val: string) => { const handleChangeDate = (val: string) => {
searchParams.set("date", val); setSearchParamsFromKeys({ date: val });
setSearchParams(searchParams);
}; };
useEffect(() => { useEffect(() => {

View file

@ -17,6 +17,7 @@ import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import DateTimeInput from "../../../Main/DatePicker/DateTimeInput/DateTimeInput"; import DateTimeInput from "../../../Main/DatePicker/DateTimeInput/DateTimeInput";
import useBoolean from "../../../../hooks/useBoolean"; import useBoolean from "../../../../hooks/useBoolean";
import useWindowSize from "../../../../hooks/useWindowSize"; import useWindowSize from "../../../../hooks/useWindowSize";
import usePrevious from "../../../../hooks/usePrevious";
export const TimeSelector: FC = () => { export const TimeSelector: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
@ -31,6 +32,7 @@ export const TimeSelector: FC = () => {
const { period: { end, start }, relativeTime, timezone, duration } = useTimeState(); const { period: { end, start }, relativeTime, timezone, duration } = useTimeState();
const dispatch = useTimeDispatch(); const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable(); const appModeEnable = getAppModeEnable();
const prevTimezone = usePrevious(timezone);
const { const {
value: openOptions, value: openOptions,
@ -95,8 +97,10 @@ export const TimeSelector: FC = () => {
defaultDuration: duration, defaultDuration: duration,
defaultEndInput: dateFromSeconds(end), defaultEndInput: dateFromSeconds(end),
}); });
setDuration({ id: value.relativeTimeId, duration: value.duration, until: value.endInput }); if (prevTimezone && timezone !== prevTimezone) {
}, [timezone]); setDuration({ id: value.relativeTimeId, duration: value.duration, until: value.endInput });
}
}, [timezone, prevTimezone]);
useClickOutside(wrapperRef, (e) => { useClickOutside(wrapperRef, (e) => {
if (isMobile) return; if (isMobile) return;

View file

@ -3,7 +3,6 @@ import { TimeStateProvider } from "../state/time/TimeStateContext";
import { QueryStateProvider } from "../state/query/QueryStateContext"; import { QueryStateProvider } from "../state/query/QueryStateContext";
import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext"; import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
import { GraphStateProvider } from "../state/graph/GraphStateContext"; import { GraphStateProvider } from "../state/graph/GraphStateContext";
import { TopQueriesStateProvider } from "../state/topQueries/TopQueriesStateContext";
import { SnackbarProvider } from "./Snackbar"; import { SnackbarProvider } from "./Snackbar";
import { combineComponents } from "../utils/combine-components"; import { combineComponents } from "../utils/combine-components";
@ -15,7 +14,6 @@ const providers = [
QueryStateProvider, QueryStateProvider,
CustomPanelStateProvider, CustomPanelStateProvider,
GraphStateProvider, GraphStateProvider,
TopQueriesStateProvider,
SnackbarProvider, SnackbarProvider,
DashboardsStateProvider DashboardsStateProvider
]; ];

View file

@ -6,18 +6,21 @@ import "./style.scss";
import Tooltip from "../../../components/Main/Tooltip/Tooltip"; import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames"; import classNames from "classnames";
import { useEffect, useState } from "preact/compat"; import { useEffect } from "preact/compat";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import CardinalityTotals, { CardinalityTotalsProps } from "../CardinalityTotals/CardinalityTotals"; import CardinalityTotals, { CardinalityTotalsProps } from "../CardinalityTotals/CardinalityTotals";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => { const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const showTips = searchParams.get("tips") || ""; const showTips = searchParams.get("tips") || "";
const [match, setMatch] = useState(searchParams.get("match") || ""); const [match, setMatch] = useStateSearchParams("", "match");
const [focusLabel, setFocusLabel] = useState(searchParams.get("focusLabel") || ""); const [focusLabel, setFocusLabel] = useStateSearchParams("", "focusLabel");
const [topN, setTopN] = useState(+(searchParams.get("topN") || 10)); const [topN, setTopN] = useStateSearchParams(10, "topN");
const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]); const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]);
@ -27,23 +30,16 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
}; };
const handleRunQuery = () => { const handleRunQuery = () => {
searchParams.set("match", match); setSearchParamsFromKeys({ match, topN, focusLabel });
searchParams.set("topN", topN.toString());
searchParams.set("focusLabel", focusLabel);
setSearchParams(searchParams);
}; };
const handleResetQuery = () => { const handleResetQuery = () => {
searchParams.set("match", ""); setSearchParamsFromKeys({ match: "", focusLabel: "" });
searchParams.set("focusLabel", "");
setSearchParams(searchParams);
}; };
const handleToggleTips = () => { const handleToggleTips = () => {
const showTips = searchParams.get("tips") || ""; const showTips = searchParams.get("tips") || "";
if (showTips) searchParams.delete("tips"); setSearchParamsFromKeys({ tips: showTips ? "" : "true" });
else searchParams.set("tips", "true");
setSearchParams(searchParams);
}; };
useEffect(() => { useEffect(() => {

View file

@ -17,6 +17,7 @@ import {
TipHighNumberOfSeries, TipHighNumberOfSeries,
TipHighNumberOfValues TipHighNumberOfValues
} from "./CardinalityTips"; } from "./CardinalityTips";
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
const spinnerMessage = `Please wait while cardinality stats is calculated. const spinnerMessage = `Please wait while cardinality stats is calculated.
This may take some time if the db contains big number of time series.`; This may take some time if the db contains big number of time series.`;
@ -24,7 +25,8 @@ const spinnerMessage = `Please wait while cardinality stats is calculated.
const CardinalityPanel: FC = () => { const CardinalityPanel: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const showTips = searchParams.get("tips") || ""; const showTips = searchParams.get("tips") || "";
const match = searchParams.get("match") || ""; const match = searchParams.get("match") || "";
const focusLabel = searchParams.get("focusLabel") || ""; const focusLabel = searchParams.get("focusLabel") || "";
@ -35,14 +37,14 @@ const CardinalityPanel: FC = () => {
const handleFilterClick = (key: string) => (query: string) => { const handleFilterClick = (key: string) => (query: string) => {
const value = queryUpdater[key]({ query, focusLabel, match }); const value = queryUpdater[key]({ query, focusLabel, match });
searchParams.set("match", value); const params: Record<string, string> = { match: value };
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") { if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
searchParams.set("focusLabel", query); params.focusLabel = query;
} }
if (key == "seriesCountByFocusLabelValue") { if (key == "seriesCountByFocusLabelValue") {
searchParams.set("focusLabel", ""); params.focusLabel = "";
} }
setSearchParams(searchParams); setSearchParamsFromKeys(params);
}; };
return ( return (

View file

@ -6,7 +6,7 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
import { displayTypeTabs } from "../DisplayTypeSwitch"; import { displayTypeTabs } from "../DisplayTypeSwitch";
import { compactObject } from "../../../utils/object"; import { compactObject } from "../../../utils/object";
import { useGraphState } from "../../../state/graph/GraphStateContext"; import { useGraphState } from "../../../state/graph/GraphStateContext";
import { useSearchParams } from "react-router-dom"; import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
export const useSetQueryParams = () => { export const useSetQueryParams = () => {
const { tenantId } = useAppState(); const { tenantId } = useAppState();
@ -14,7 +14,7 @@ export const useSetQueryParams = () => {
const { query } = useQueryState(); const { query } = useQueryState();
const { duration, relativeTime, period: { date, step } } = useTimeState(); const { duration, relativeTime, period: { date, step } } = useTimeState();
const { customStep } = useGraphState(); const { customStep } = useGraphState();
const [, setSearchParams] = useSearchParams(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const setSearchParamsFromState = () => { const setSearchParamsFromState = () => {
const params: Record<string, unknown> = {}; const params: Record<string, unknown> = {};
@ -30,7 +30,7 @@ export const useSetQueryParams = () => {
if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep; if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
}); });
setSearchParams(compactObject(params) as Record<string, string>); setSearchParamsFromKeys(compactObject(params) as Record<string, string>);
}; };
useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]); useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);

View file

@ -26,6 +26,7 @@ import GraphTips from "../../components/Chart/GraphTips/GraphTips";
import InstantQueryTip from "./InstantQueryTip/InstantQueryTip"; import InstantQueryTip from "./InstantQueryTip/InstantQueryTip";
import useBoolean from "../../hooks/useBoolean"; import useBoolean from "../../hooks/useBoolean";
import { getColumns } from "../../hooks/useSortedCategories"; import { getColumns } from "../../hooks/useSortedCategories";
import useEventListener from "../../hooks/useEventListener";
const CustomPanel: FC = () => { const CustomPanel: FC = () => {
const { displayType, isTracingEnabled } = useCustomPanelState(); const { displayType, isTracingEnabled } = useCustomPanelState();
@ -100,6 +101,9 @@ const CustomPanel: FC = () => {
customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" }); customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" });
}; };
const handleChangePopstate = () => window.location.reload();
useEventListener("popstate", handleChangePopstate);
useEffect(() => { useEffect(() => {
if (traces) { if (traces) {
setTracesState([...tracesState, ...traces]); setTracesState([...tracesState, ...traces]);

View file

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { compactObject } from "../../../utils/object"; import { compactObject } from "../../../utils/object";
import { useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeState } from "../../../state/time/TimeStateContext";
import { useGraphState } from "../../../state/graph/GraphStateContext"; import { useGraphState } from "../../../state/graph/GraphStateContext";
import { useSearchParams } from "react-router-dom"; import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
interface queryProps { interface queryProps {
job: string job: string
@ -14,7 +14,7 @@ interface queryProps {
export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps) => { export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps) => {
const { duration, relativeTime, period: { date } } = useTimeState(); const { duration, relativeTime, period: { date } } = useTimeState();
const { customStep } = useGraphState(); const { customStep } = useGraphState();
const [, setSearchParams] = useSearchParams(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const setSearchParamsFromState = () => { const setSearchParamsFromState = () => {
const params = compactObject({ const params = compactObject({
@ -28,7 +28,7 @@ export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps)
metrics metrics
}); });
setSearchParams(params); setSearchParamsFromKeys(params);
}; };
useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep, job, instance, metrics, size]); useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep, job, instance, metrics, size]);

View file

@ -2,12 +2,12 @@ import { useEffect } from "react";
import { compactObject } from "../../../utils/object"; import { compactObject } from "../../../utils/object";
import { useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeState } from "../../../state/time/TimeStateContext";
import { useGraphState } from "../../../state/graph/GraphStateContext"; import { useGraphState } from "../../../state/graph/GraphStateContext";
import { useSearchParams } from "react-router-dom"; import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
export const useSetQueryParams = () => { export const useSetQueryParams = () => {
const { duration, relativeTime, period: { date } } = useTimeState(); const { duration, relativeTime, period: { date } } = useTimeState();
const { customStep } = useGraphState(); const { customStep } = useGraphState();
const [, setSearchParams] = useSearchParams(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const setSearchParamsFromState = () => { const setSearchParamsFromState = () => {
const params = compactObject({ const params = compactObject({
@ -17,7 +17,7 @@ export const useSetQueryParams = () => {
["g0.relative_time"]: relativeTime ["g0.relative_time"]: relativeTime
}); });
setSearchParams(params); setSearchParamsFromKeys(params);
}; };
useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep]); useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep]);

View file

@ -1,7 +1,6 @@
import React, { FC, useEffect } from "preact/compat"; import React, { FC, useEffect } from "preact/compat";
import "./style.scss"; import "./style.scss";
import TextField from "../../components/Main/TextField/TextField"; import TextField from "../../components/Main/TextField/TextField";
import { useState } from "react";
import Button from "../../components/Main/Button/Button"; import Button from "../../components/Main/Button/Button";
import { InfoIcon, PlayIcon, WikiIcon } from "../../components/Main/Icons"; import { InfoIcon, PlayIcon, WikiIcon } from "../../components/Main/Icons";
import "./style.scss"; import "./style.scss";
@ -9,6 +8,7 @@ import { useRelabelDebug } from "./hooks/useRelabelDebug";
import Spinner from "../../components/Main/Spinner/Spinner"; import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert"; import Alert from "../../components/Main/Alert/Alert";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import useStateSearchParams from "../../hooks/useStateSearchParams";
const example = { const example = {
config: `- if: '{bar_label=~"b.*"}' config: `- if: '{bar_label=~"b.*"}'
@ -27,8 +27,8 @@ const Relabel: FC = () => {
const { data, loading, error, fetchData } = useRelabelDebug(); const { data, loading, error, fetchData } = useRelabelDebug();
const [config, setConfig] = useState(""); const [config, setConfig] = useStateSearchParams("", "config");
const [labels, setLabels] = useState(""); const [labels, setLabels] = useStateSearchParams("", "labels");
const handleChangeConfig = (val: string) => { const handleChangeConfig = (val: string) => {
setConfig(val); setConfig(val);

View file

@ -6,12 +6,12 @@ import classNames from "classnames";
import { ArrowDropDownIcon, CopyIcon, PlayCircleOutlineIcon } from "../../../components/Main/Icons"; import { ArrowDropDownIcon, CopyIcon, PlayCircleOutlineIcon } from "../../../components/Main/Icons";
import Button from "../../../components/Main/Button/Button"; import Button from "../../../components/Main/Button/Button";
import Tooltip from "../../../components/Main/Tooltip/Tooltip"; import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { useSnack } from "../../../contexts/Snackbar";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import router from "../../../router"; import router from "../../../router";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy }) => { const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy }) => {
const { showInfoMessage } = useSnack(); const copyToClipboard = useCopyToClipboard();
const [orderBy, setOrderBy] = useState<keyof TopQuery>(defaultOrderBy || "count"); const [orderBy, setOrderBy] = useState<keyof TopQuery>(defaultOrderBy || "count");
const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc"); const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc");
@ -28,10 +28,8 @@ const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy })
onSortHandler(col); onSortHandler(col);
}; };
const createCopyHandler = ({ query }: TopQuery) => () => { const createCopyHandler = ({ query }: TopQuery) => async () => {
// TODO add useCopyToClipboard after merge https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4145 await copyToClipboard(query, "Query has been copied");
navigator.clipboard.writeText(query);
showInfoMessage({ text: "Query has been copied", type: "success" });
}; };
return ( return (

View file

@ -1,15 +1,19 @@
import { useEffect, useState } from "react";
import { ErrorTypes } from "../../../types"; import { ErrorTypes } from "../../../types";
import { useAppState } from "../../../state/common/StateContext"; import { useAppState } from "../../../state/common/StateContext";
import { useMemo } from "preact/compat"; import { useMemo, useState } from "preact/compat";
import { getTopQueries } from "../../../api/top-queries"; import { getTopQueries } from "../../../api/top-queries";
import { TopQueriesData } from "../../../types"; import { TopQueriesData } from "../../../types";
import { useTopQueriesState } from "../../../state/topQueries/TopQueriesStateContext";
import { getDurationFromMilliseconds } from "../../../utils/time"; import { getDurationFromMilliseconds } from "../../../utils/time";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
export const useFetchTopQueries = () => { interface useFetchTopQueriesProps {
topN: number;
maxLifetime: string;
}
export const useFetchTopQueries = ({ topN, maxLifetime }: useFetchTopQueriesProps) => {
const { serverUrl } = useAppState(); const { serverUrl } = useAppState();
const { topN, maxLifetime, runQuery } = useTopQueriesState(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [data, setData] = useState<TopQueriesData | null>(null); const [data, setData] = useState<TopQueriesData | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -19,6 +23,7 @@ export const useFetchTopQueries = () => {
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
setSearchParamsFromKeys({ topN, maxLifetime });
try { try {
const response = await fetch(fetchUrl); const response = await fetch(fetchUrl);
const resp = await response.json(); const resp = await response.json();
@ -42,13 +47,10 @@ export const useFetchTopQueries = () => {
setLoading(false); setLoading(false);
}; };
useEffect(() => {
fetchData();
}, [runQuery]);
return { return {
data, data,
error, error,
loading loading,
fetch: fetchData
}; };
}; };

View file

@ -1,21 +0,0 @@
import { useTopQueriesState } from "../../../state/topQueries/TopQueriesStateContext";
import { useEffect } from "react";
import { compactObject } from "../../../utils/object";
import { useSearchParams } from "react-router-dom";
export const useSetQueryParams = () => {
const { topN, maxLifetime } = useTopQueriesState();
const [, setSearchParams] = useSearchParams();
const setSearchParamsFromState = () => {
const params = compactObject({
topN: String(topN),
maxLifetime: maxLifetime,
});
setSearchParams(params);
};
useEffect(setSearchParamsFromState, [topN, maxLifetime]);
useEffect(setSearchParamsFromState, []);
};

View file

@ -2,12 +2,10 @@ import React, { FC, useEffect, useMemo, KeyboardEvent } from "react";
import { useFetchTopQueries } from "./hooks/useFetchTopQueries"; import { useFetchTopQueries } from "./hooks/useFetchTopQueries";
import Spinner from "../../components/Main/Spinner/Spinner"; import Spinner from "../../components/Main/Spinner/Spinner";
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel"; import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
import { useTopQueriesDispatch, useTopQueriesState } from "../../state/topQueries/TopQueriesStateContext";
import { formatPrettyNumber } from "../../utils/uplot/helpers"; import { formatPrettyNumber } from "../../utils/uplot/helpers";
import { isSupportedDuration } from "../../utils/time"; import { isSupportedDuration } from "../../utils/time";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { TopQueryStats } from "../../types"; import { TopQueryStats } from "../../types";
import { useSetQueryParams } from "./hooks/useSetQueryParams";
import Button from "../../components/Main/Button/Button"; import Button from "../../components/Main/Button/Button";
import { PlayIcon } from "../../components/Main/Icons"; import { PlayIcon } from "../../components/Main/Icons";
import TextField from "../../components/Main/TextField/TextField"; import TextField from "../../components/Main/TextField/TextField";
@ -16,15 +14,17 @@ import Tooltip from "../../components/Main/Tooltip/Tooltip";
import "./style.scss"; import "./style.scss";
import useDeviceDetect from "../../hooks/useDeviceDetect"; import useDeviceDetect from "../../hooks/useDeviceDetect";
import classNames from "classnames"; import classNames from "classnames";
import useStateSearchParams from "../../hooks/useStateSearchParams";
const exampleDuration = "30ms, 15s, 3d4h, 1y2w"; const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
const TopQueries: FC = () => { const TopQueries: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { data, error, loading } = useFetchTopQueries();
const { topN, maxLifetime } = useTopQueriesState(); const [topN, setTopN] = useStateSearchParams(10, "topN");
const topQueriesDispatch = useTopQueriesDispatch(); const [maxLifetime, setMaxLifetime] = useStateSearchParams("10m", "maxLifetime");
useSetQueryParams();
const { data, error, loading, fetch } = useFetchTopQueries({ topN, maxLifetime });
const maxLifetimeValid = useMemo(() => { const maxLifetimeValid = useMemo(() => {
const durItems = maxLifetime.trim().split(" "); const durItems = maxLifetime.trim().split(" ");
@ -48,27 +48,32 @@ const TopQueries: FC = () => {
}; };
const onTopNChange = (value: string) => { const onTopNChange = (value: string) => {
topQueriesDispatch({ type: "SET_TOP_N", payload: +value }); setTopN(+value);
}; };
const onMaxLifetimeChange = (value: string) => { const onMaxLifetimeChange = (value: string) => {
topQueriesDispatch({ type: "SET_MAX_LIFE_TIME", payload: value }); setMaxLifetime(value);
};
const onApplyQuery = () => {
topQueriesDispatch({ type: "SET_RUN_QUERY" });
}; };
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") onApplyQuery(); if (e.key === "Enter") fetch();
}; };
useEffect(() => { useEffect(() => {
if (!data) return; if (!data) return;
if (!topN) topQueriesDispatch({ type: "SET_TOP_N", payload: +data.topN }); if (!topN) setTopN(+data.topN);
if (!maxLifetime) topQueriesDispatch({ type: "SET_MAX_LIFE_TIME", payload: data.maxLifetime }); if (!maxLifetime) setMaxLifetime(data.maxLifetime);
}, [data]); }, [data]);
useEffect(() => {
fetch();
window.addEventListener("popstate", fetch);
return () => {
window.removeEventListener("popstate", fetch);
};
}, []);
return ( return (
<div <div
className={classNames({ className={classNames({
@ -130,7 +135,7 @@ const TopQueries: FC = () => {
<div className="vm-top-queries-controls-bottom__button"> <div className="vm-top-queries-controls-bottom__button">
<Button <Button
startIcon={<PlayIcon/>} startIcon={<PlayIcon/>}
onClick={onApplyQuery} onClick={fetch}
> >
Execute Execute
</Button> </Button>

View file

@ -9,7 +9,6 @@ import { CloseIcon } from "../../components/Main/Icons";
import Modal from "../../components/Main/Modal/Modal"; import Modal from "../../components/Main/Modal/Modal";
import JsonForm from "./JsonForm/JsonForm"; import JsonForm from "./JsonForm/JsonForm";
import { ErrorTypes } from "../../types"; import { ErrorTypes } from "../../types";
import { useSearchParams } from "react-router-dom";
import useDropzone from "../../hooks/useDropzone"; import useDropzone from "../../hooks/useDropzone";
import TraceUploadButtons from "./TraceUploadButtons/TraceUploadButtons"; import TraceUploadButtons from "./TraceUploadButtons/TraceUploadButtons";
import useBoolean from "../../hooks/useBoolean"; import useBoolean from "../../hooks/useBoolean";
@ -18,7 +17,6 @@ const TracePage: FC = () => {
const [tracesState, setTracesState] = useState<Trace[]>([]); const [tracesState, setTracesState] = useState<Trace[]>([]);
const [errors, setErrors] = useState<{filename: string, text: string}[]>([]); const [errors, setErrors] = useState<{filename: string, text: string}[]>([]);
const hasTraces = useMemo(() => !!tracesState.length, [tracesState]); const hasTraces = useMemo(() => !!tracesState.length, [tracesState]);
const [, setSearchParams] = useSearchParams();
const { const {
value: openModal, value: openModal,
@ -77,11 +75,6 @@ const TracePage: FC = () => {
handleCloseError(index); handleCloseError(index);
}; };
useEffect(() => {
setSearchParams({});
}, []);
const { files, dragging } = useDropzone(); const { files, dragging } = useDropzone();
useEffect(() => { useEffect(() => {

View file

@ -1,23 +0,0 @@
import React, { createContext, FC, useContext, useMemo, useReducer } from "preact/compat";
import { Action, TopQueriesState, initialState, reducer } from "./reducer";
import { Dispatch } from "react";
type TopQueriesStateContextType = { state: TopQueriesState, dispatch: Dispatch<Action> };
export const TopQueriesStateContext = createContext<TopQueriesStateContextType>({} as TopQueriesStateContextType);
export const useTopQueriesState = (): TopQueriesState => useContext(TopQueriesStateContext).state;
export const useTopQueriesDispatch = (): Dispatch<Action> => useContext(TopQueriesStateContext).dispatch;
export const TopQueriesStateProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const contextValue = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
return <TopQueriesStateContext.Provider value={contextValue}>
{children}
</TopQueriesStateContext.Provider>;
};

View file

@ -1,41 +0,0 @@
import { getQueryStringValue } from "../../utils/query-string";
export interface TopQueriesState {
maxLifetime: string,
topN: number | null,
runQuery: number
}
export type Action =
| { type: "SET_TOP_N", payload: number | null }
| { type: "SET_MAX_LIFE_TIME", payload: string }
| { type: "SET_RUN_QUERY" }
export const initialState: TopQueriesState = {
topN: getQueryStringValue("topN", null) as number,
maxLifetime: getQueryStringValue("maxLifetime", "") as string,
runQuery: 0
};
export function reducer(state: TopQueriesState, action: Action): TopQueriesState {
switch (action.type) {
case "SET_TOP_N":
return {
...state,
topN: action.payload
};
case "SET_MAX_LIFE_TIME":
return {
...state,
maxLifetime: action.payload
};
case "SET_RUN_QUERY":
return {
...state,
runQuery: state.runQuery + 1
};
default:
throw new Error();
}
}

View file

@ -1,4 +1,5 @@
import { getAppModeParams } from "./app-mode"; import { getAppModeParams } from "./app-mode";
import { replaceTenantId } from "./tenants";
const { REACT_APP_LOGS } = process.env; const { REACT_APP_LOGS } = process.env;
export const getDefaultServer = (tenantId?: string): string => { export const getDefaultServer = (tenantId?: string): string => {
@ -9,8 +10,3 @@ export const getDefaultServer = (tenantId?: string): string => {
if (tenantId) return replaceTenantId(url, tenantId); if (tenantId) return replaceTenantId(url, tenantId);
return url; return url;
}; };
export const replaceTenantId = (serverUrl: string, tenantId: string) => {
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
return serverUrl.replace(regexp, `$1${tenantId}/$4`);
};

View file

@ -12,5 +12,5 @@ export function filterObject<T extends object>(
} }
export function compactObject<T extends object>(obj: T) { export function compactObject<T extends object>(obj: T) {
return filterObject(obj, (entry) => !!entry[1]); return filterObject(obj, (entry) => !!entry[1] || typeof entry[1] === "number");
} }

View file

@ -50,6 +50,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly return error from [/api/v1/query](https://docs.victoriametrics.com/keyConcepts.html#instant-query) and [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) at `vmselect` when the `-search.maxSamplesPerQuery` or `-search.maxSamplesPerSeries` [limit](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#resource-usage-limits) is exceeded. Previously incomplete response could be returned without the error if `vmselect` runs with `-replicationFactor` greater than 1. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4472). * BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly return error from [/api/v1/query](https://docs.victoriametrics.com/keyConcepts.html#instant-query) and [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) at `vmselect` when the `-search.maxSamplesPerQuery` or `-search.maxSamplesPerSeries` [limit](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#resource-usage-limits) is exceeded. Previously incomplete response could be returned without the error if `vmselect` runs with `-replicationFactor` greater than 1. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4472).
* BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): Properly creates `parts.json` after migration from versions below `v1.90.0. It must fix errors on start-up after unclean shutdown. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4336) for details. * BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): Properly creates `parts.json` after migration from versions below `v1.90.0. It must fix errors on start-up after unclean shutdown. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4336) for details.
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix a memory leak issue associated with chart updates. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4455). * BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix a memory leak issue associated with chart updates. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4455).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix application routing issues and problems with manual URL changes. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4408).
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): retry all errors except 4XX status codes while pushing via remote-write to the remote storage. Previously, errors like broken connection could prevent vmalert from retrying the request. * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): retry all errors except 4XX status codes while pushing via remote-write to the remote storage. Previously, errors like broken connection could prevent vmalert from retrying the request.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): properly interrupt retry attempts on vmalert shutdown. Before, vmalert could have waited for all retries to finish for shutdown. * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): properly interrupt retry attempts on vmalert shutdown. Before, vmalert could have waited for all retries to finish for shutdown.