Compare commits

...

1 Commits

Author SHA1 Message Date
Yury Molodov
5d36d8fdd8 vmui/logs: fix groups auto-expand on list update #8076
Signed-off-by: Yury Molodov <yurymolodov@gmail.com>
2025-07-03 17:56:49 +02:00
12 changed files with 116 additions and 28 deletions

View File

@@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useRef } from "preact/compat";
import { FC, useEffect, useMemo, useRef } from "preact/compat";
import { GraphOptions, GRAPH_STYLES } from "../types";
import Switch from "../../../Main/Switch/Switch";
import "./style.scss";
@@ -61,15 +61,6 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
return (
<div className="vm-bar-hits-options">
<Tooltip title={hideChart ? "Show chart and resume hits updates" : "Hide chart and pause hits updates"}>
<Button
variant="text"
color="primary"
startIcon={hideChart ? <VisibilityOffIcon/> : <VisibilityIcon/>}
onClick={toggleHideChart}
ariaLabel="settings"
/>
</Tooltip>
<div ref={optionsButtonRef}>
<Tooltip title="Graph settings">
<Button
@@ -81,6 +72,15 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
/>
</Tooltip>
</div>
<Tooltip title={hideChart ? "Show chart and resume hits updates" : "Hide chart and pause hits updates"}>
<Button
variant="text"
color="primary"
startIcon={hideChart ? <VisibilityOffIcon/> : <VisibilityIcon/>}
onClick={toggleHideChart}
ariaLabel="settings"
/>
</Tooltip>
<Popper
open={openOptions}
placement="bottom-right"

View File

@@ -6,7 +6,7 @@
left: 0;
right: 0;
height: 2px;
z-index: 2;
z-index: 9;
overflow: hidden;
&__background {

View File

@@ -23,6 +23,8 @@ import usePrevious from "../../hooks/usePrevious";
const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
type FetchFlags = { logs: boolean; hits: boolean };
const ExploreLogs: FC = () => {
const { serverUrl } = useAppState();
const { queryHistory } = useQueryState();
@@ -30,9 +32,13 @@ const ExploreLogs: FC = () => {
const { duration, relativeTime, period: periodState } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [searchParams] = useSearchParams();
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
const prevHideChart = usePrevious(hideChart);
const hideLogs = useMemo(() => searchParams.get("hide_logs"), [searchParams]);
const prevHideLogs = usePrevious(hideLogs);
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
const [query, setQuery] = useStateSearchParams("*", "query");
@@ -54,10 +60,15 @@ const ExploreLogs: FC = () => {
const { logs, isLoading, error, fetchLogs, abortController } = useFetchLogs(serverUrl, query, limit);
const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query);
const fetchData = (p: TimeParams, hits: boolean) => {
fetchLogs(p).then((isSuccess) => {
if (isSuccess && hits) fetchLogHits(p);
}).catch(() => {/* error handled elsewhere */});
const fetchData = async (p: TimeParams, flags: FetchFlags) => {
if (flags.logs) {
const isSuccess = await fetchLogs(p);
if (!isSuccess) return;
}
if (flags.hits) {
await fetchLogHits(p);
}
};
const debouncedFetchLogs = useDebounceCallback(fetchData, 300);
@@ -78,7 +89,7 @@ const ExploreLogs: FC = () => {
const newPeriod = getPeriod();
setPeriod(newPeriod);
debouncedFetchLogs(newPeriod, !hideChart);
debouncedFetchLogs(newPeriod, { logs: !hideLogs, hits: !hideChart });
setSearchParamsFromKeys({
query,
"g0.range_input": duration,
@@ -125,6 +136,12 @@ const ExploreLogs: FC = () => {
}
}, [hideChart, prevHideChart, period]);
useEffect(() => {
if (!hideLogs && prevHideLogs) {
fetchLogs(period);
}
}, [hideLogs, prevHideLogs, period]);
return (
<div className="vm-explore-logs">
<ExploreLogsHeader

View File

@@ -1,4 +1,4 @@
import React, { FC, useCallback, useMemo } from "preact/compat";
import { FC, useCallback, useMemo } from "preact/compat";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
@@ -69,6 +69,8 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onA
}, [logHits]);
const noDataMessage: string = useMemo(() => {
if (isLoading) return "";
const noData = data.every(d => d.length === 0);
const noTimestamps = data[0].length === 0;
const noValues = data[1].length === 0;
@@ -81,7 +83,7 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onA
} else if (noValues) {
return "No value information available for the current queries and time range.";
} return "";
}, [data, hideChart]);
}, [data, hideChart, isLoading]);
const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
timeDispatch({ type: "SET_PERIOD", payload: { from, to } });

View File

@@ -1,5 +1,12 @@
import { FC, useRef } from "preact/compat";
import { CodeIcon, ListIcon, TableIcon, PlayIcon } from "../../../components/Main/Icons";
import {
CodeIcon,
ListIcon,
TableIcon,
PlayIcon,
VisibilityOffIcon,
VisibilityIcon
} from "../../../components/Main/Icons";
import Tabs from "../../../components/Main/Tabs/Tabs";
import "./style.scss";
import classNames from "classnames";
@@ -12,6 +19,10 @@ import GroupView from "./views/GroupView/GroupView";
import TableView from "./views/TableView/TableView";
import JsonView from "./views/JsonView/JsonView";
import LiveTailingView from "./views/LiveTailingView/LiveTailingView";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import Button from "../../../components/Main/Button/Button";
import { useSearchParams } from "react-router-dom";
import Alert from "../../../components/Main/Alert/Alert";
export interface ExploreLogBodyProps {
data: Logs[];
@@ -34,10 +45,22 @@ const tabs = [
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
const settingsRef = useRef<HTMLDivElement>(null);
const [hideLogs, setHideLogs] = useStateSearchParams(false, "hide_logs");
const toggleHideLogs = () => {
setHideLogs(prev => {
const newVal = !prev;
newVal ? searchParams.set("hide_logs", "true") : searchParams.delete("hide_logs");
setSearchParams(searchParams);
return newVal;
});
};
const handleChangeTab = (view: string) => {
setActiveTab(view as DisplayType);
setSearchParamsFromKeys({ view });
@@ -82,19 +105,33 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
className="vm-explore-logs-body-header__settings"
ref={settingsRef}
/>
<Tooltip title={hideLogs ? "Show Logs" : "Hide Logs"}>
<Button
variant="text"
color="primary"
startIcon={hideLogs ? <VisibilityOffIcon/> : <VisibilityIcon/>}
onClick={toggleHideLogs}
ariaLabel="settings"
/>
</Tooltip>
</div>
<div
className={classNames({
"vm-explore-logs-body__table": true,
"vm-explore-logs-body__table_hide": hideLogs,
"vm-explore-logs-body__table_mobile": isMobile,
})}
>
{ActiveTabComponent &&
<ActiveTabComponent
data={data}
settingsRef={settingsRef}
/>
{hideLogs && (
<Alert variant="info">Logs are hidden. Updates paused.</Alert>
)}
{!hideLogs && ActiveTabComponent &&
<ActiveTabComponent
data={data}
settingsRef={settingsRef}
/>
}
</div>
</div>

View File

@@ -4,6 +4,7 @@
position: relative;
&-header {
grid-template-columns: 1fr auto auto;
background-color: $color-background-block;
z-index: 3;
margin: -$padding-medium 0-$padding-medium 0;
@@ -68,5 +69,11 @@
.vm-table {
min-width: 700px;
}
&_hide {
display: flex;
align-content: center;
justify-content: center;
}
}
}

View File

@@ -93,7 +93,6 @@ const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
<Button
ref={settingButtonRef}
variant="text"
color="secondary"
onClick={openSettings}
startIcon={<SettingsIcon/>}
ariaLabel={"Settings"}

View File

@@ -5,6 +5,5 @@
&__settings-buttons {
display: flex;
align-items: center;
gap: $padding-small;
}
}
}

View File

@@ -97,7 +97,10 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const getLogs = useCallback(() => logs, [logs]);
useEffect(() => {
setExpandGroups(new Array(groupData.length).fill(!isMobile));
setExpandGroups(prev => {
const keepClosed = (prev.every(v => !v) && prev.length) || isMobile;
return new Array(groupData.length).fill(!keepClosed);
});
}, [groupData]);
useEffect(() => {

View File

@@ -7,9 +7,13 @@ import { LOGS_GROUP_BY, LOGS_LIMIT_HITS } from "../../../constants/logs";
import { isEmptyObject } from "../../../utils/object";
import { useEffect } from "react";
import { useTenant } from "../../../hooks/useTenant";
import { useSearchParams } from "react-router-dom";
export const useFetchLogHits = (server: string, query: string) => {
const tenant = useTenant();
const [searchParams] = useSearchParams();
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
const [logHits, setLogHits] = useState<LogHits[]>([]);
const [isLoading, setIsLoading] = useState<{[key: number]: boolean;}>([]);
const [error, setError] = useState<ErrorTypes | string>();
@@ -82,6 +86,13 @@ export const useFetchLogHits = (server: string, query: string) => {
};
}, []);
useEffect(() => {
if (hideChart) {
setLogHits([]);
setError(undefined);
}
}, [hideChart]);
return {
logHits,
isLoading: Object.values(isLoading).some(s => s),

View File

@@ -4,9 +4,12 @@ import { ErrorTypes, TimeParams } from "../../../types";
import { Logs } from "../../../api/types";
import dayjs from "dayjs";
import { useTenant } from "../../../hooks/useTenant";
import { useSearchParams } from "react-router-dom";
export const useFetchLogs = (server: string, query: string, limit: number) => {
const tenant = useTenant();
const [searchParams] = useSearchParams();
const hideLogs = useMemo(() => searchParams.get("hide_logs"), [searchParams]);
const [logs, setLogs] = useState<Logs[]>([]);
const [isLoading, setIsLoading] = useState<{ [key: number]: boolean }>({});
@@ -75,6 +78,13 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
};
}, []);
useEffect(() => {
if (hideLogs) {
setLogs([]);
setError(undefined);
}
}, [hideLogs]);
return {
logs,
isLoading: Object.values(isLoading).some(s => s),

View File

@@ -18,8 +18,11 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add the ability to hide the logs panel to view only the graph. When the logs panel is hidden, the `/query` request is not executed.
* BUGFIX: [`rate_sum` stats function](https://docs.victoriametrics.com/victorialogs/logsql/#rate_sum-stats): fix inconsistent per-second rate calculation when time filters are specified via HTTP query parameters instead of LogsQL expression. This affects recording rule results. See [#9303](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9303).
* BUGFIX: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): disabled opening of autocomplete popup on initial page load.
* BUGFIX: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): prevent groups from automatically expanding on list updates if all groups were previously collapsed. See [#8076](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8076).
## [v1.24.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.24.0-victorialogs)