vmui/logs: add ability to hide hits chart (#7206)

### Describe Your Changes

**Added ability to hide the hits chart**

- Users can now hide or show the hits chart by clicking the "eye" icon
located in the upper-right corner of the chart.
- When the chart is hidden, it will stop sending requests to
`/select/logsql/hits`.
- Upon displaying the chart again, it will automatically refresh. If a
relative time range is set, the chart will update according to the time
period of the logs currently being displayed.

**Hits chart visible:**

![image](https://github.com/user-attachments/assets/577e877b-6417-4b83-8d84-c55e3d39864a)

**Hits chart hidden:**

![image](https://github.com/user-attachments/assets/068b1143-d140-4d72-8d65-663900124f32)

Related issue: #7117

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2024-10-18 02:30:56 +02:00 committed by GitHub
parent 36a86c3aaf
commit 423df09d7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 86 additions and 33 deletions

View file

@ -33,12 +33,15 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
graphStyle: GRAPH_STYLES.LINE_STEPPED, graphStyle: GRAPH_STYLES.LINE_STEPPED,
stacked: false, stacked: false,
fill: false, fill: false,
hideChart: false,
}); });
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod }); const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
const { onReadyChart, isPanning } = useReadyChart(setPlotScale); const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
useZoomChart({ uPlotInst, xRange, setPlotScale }); useZoomChart({ uPlotInst, xRange, setPlotScale });
const isEmptyData = useMemo(() => _data.every(d => d.length === 0), [_data]);
const { data, bands } = useMemo(() => { const { data, bands } = useMemo(() => {
return graphOptions.stacked ? stack(_data, () => false) : { data: _data, bands: [] }; return graphOptions.stacked ? stack(_data, () => false) : { data: _data, bands: [] };
}, [graphOptions, _data]); }, [graphOptions, _data]);
@ -88,26 +91,33 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
}, [data]); }, [data]);
return ( return (
<div className="vm-bar-hits-chart__wrapper"> <div
<div className={classNames({
className={classNames({ "vm-bar-hits-chart__wrapper": true,
"vm-bar-hits-chart": true, "vm-bar-hits-chart__wrapper_hidden": graphOptions.hideChart
"vm-bar-hits-chart_panning": isPanning })}
})} >
ref={containerRef} {!graphOptions.hideChart && (
>
<div <div
className="vm-line-chart__u-plot" className={classNames({
ref={uPlotRef} "vm-bar-hits-chart": true,
/> "vm-bar-hits-chart_panning": isPanning
<BarHitsTooltip })}
uPlotInst={uPlotInst} ref={containerRef}
data={_data} >
focusDataIdx={focusDataIdx} <div
/> className="vm-line-chart__u-plot"
</div> ref={uPlotRef}
/>
<BarHitsTooltip
uPlotInst={uPlotInst}
data={_data}
focusDataIdx={focusDataIdx}
/>
</div>
)}
<BarHitsOptions onChange={setGraphOptions}/> <BarHitsOptions onChange={setGraphOptions}/>
{uPlotInst && ( {uPlotInst && !isEmptyData && !graphOptions.hideChart && (
<BarHitsLegend <BarHitsLegend
uPlotInst={uPlotInst} uPlotInst={uPlotInst}
onApplyFilter={onApplyFilter} onApplyFilter={onApplyFilter}

View file

@ -6,7 +6,7 @@ import useStateSearchParams from "../../../../hooks/useStateSearchParams";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import Button from "../../../Main/Button/Button"; import Button from "../../../Main/Button/Button";
import classNames from "classnames"; import classNames from "classnames";
import { SettingsIcon } from "../../../Main/Icons"; import { SettingsIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
import Tooltip from "../../../Main/Tooltip/Tooltip"; import Tooltip from "../../../Main/Tooltip/Tooltip";
import Popper from "../../../Main/Popper/Popper"; import Popper from "../../../Main/Popper/Popper";
import useBoolean from "../../../../hooks/useBoolean"; import useBoolean from "../../../../hooks/useBoolean";
@ -27,12 +27,14 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph"); const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph");
const [stacked, setStacked] = useStateSearchParams(false, "stacked"); const [stacked, setStacked] = useStateSearchParams(false, "stacked");
const [fill, setFill] = useStateSearchParams(false, "fill"); const [fill, setFill] = useStateSearchParams(false, "fill");
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
const options: GraphOptions = useMemo(() => ({ const options: GraphOptions = useMemo(() => ({
graphStyle, graphStyle,
stacked, stacked,
fill, fill,
}), [graphStyle, stacked, fill]); hideChart,
}), [graphStyle, stacked, fill, hideChart]);
const handleChangeGraphStyle = (val: string) => () => { const handleChangeGraphStyle = (val: string) => () => {
setGraphStyle(val as GRAPH_STYLES); setGraphStyle(val as GRAPH_STYLES);
@ -52,24 +54,41 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
setSearchParams(searchParams); setSearchParams(searchParams);
}; };
const toggleHideChart = () => {
setHideChart(prev => {
const newVal = !prev;
newVal ? searchParams.set("hide_chart", "true") : searchParams.delete("hide_chart");
setSearchParams(searchParams);
return newVal;
});
};
useEffect(() => { useEffect(() => {
onChange(options); onChange(options);
}, [options]); }, [options]);
return ( return (
<div <div className="vm-bar-hits-options">
className="vm-bar-hits-options" <Tooltip title={hideChart ? "Show chart and resume hits updates" : "Hide chart and pause hits updates"}>
ref={optionsButtonRef}
>
<Tooltip title="Graph settings">
<Button <Button
variant="text" variant="text"
color="primary" color="primary"
startIcon={<SettingsIcon/>} startIcon={hideChart ? <VisibilityOffIcon/> : <VisibilityIcon/>}
onClick={toggleOpenOptions} onClick={toggleHideChart}
ariaLabel="settings" ariaLabel="settings"
/> />
</Tooltip> </Tooltip>
<div ref={optionsButtonRef}>
<Tooltip title="Graph settings">
<Button
variant="text"
color="primary"
startIcon={<SettingsIcon/>}
onClick={toggleOpenOptions}
ariaLabel="settings"
/>
</Tooltip>
</div>
<Popper <Popper
open={openOptions} open={openOptions}
placement="bottom-right" placement="bottom-right"

View file

@ -1,6 +1,8 @@
@use "src/styles/variables" as *; @use "src/styles/variables" as *;
.vm-bar-hits-options { .vm-bar-hits-options {
display: flex;
align-items: center;
position: absolute; position: absolute;
top: $padding-small; top: $padding-small;
right: $padding-small; right: $padding-small;

View file

@ -10,6 +10,10 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
&_hidden {
min-height: 90px;
}
} }
&_panning { &_panning {

View file

@ -9,4 +9,5 @@ export interface GraphOptions {
graphStyle: GRAPH_STYLES; graphStyle: GRAPH_STYLES;
stacked: boolean; stacked: boolean;
fill: boolean; fill: boolean;
hideChart: boolean;
} }

View file

@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useState } from "preact/compat"; import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody"; import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody";
import useStateSearchParams from "../../hooks/useStateSearchParams"; import useStateSearchParams from "../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject"; import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
@ -14,6 +14,7 @@ import ExploreLogsBarChart from "./ExploreLogsBarChart/ExploreLogsBarChart";
import { useFetchLogHits } from "./hooks/useFetchLogHits"; import { useFetchLogHits } from "./hooks/useFetchLogHits";
import { LOGS_ENTRIES_LIMIT } from "../../constants/logs"; import { LOGS_ENTRIES_LIMIT } from "../../constants/logs";
import { getTimeperiodForDuration, relativeTimeOptions } from "../../utils/time"; import { getTimeperiodForDuration, relativeTimeOptions } from "../../utils/time";
import { useSearchParams } from "react-router-dom";
const storageLimit = Number(getFromStorage("LOGS_LIMIT")); const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit; const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
@ -22,6 +23,8 @@ const ExploreLogs: FC = () => {
const { serverUrl } = useAppState(); const { serverUrl } = useAppState();
const { duration, relativeTime, period: periodState } = useTimeState(); const { duration, relativeTime, period: periodState } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [searchParams] = useSearchParams();
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit"); const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
const [query, setQuery] = useStateSearchParams("*", "query"); const [query, setQuery] = useStateSearchParams("*", "query");
@ -49,7 +52,7 @@ const ExploreLogs: FC = () => {
const newPeriod = getPeriod(); const newPeriod = getPeriod();
setPeriod(newPeriod); setPeriod(newPeriod);
fetchLogs(newPeriod).then((isSuccess) => { fetchLogs(newPeriod).then((isSuccess) => {
isSuccess && fetchLogHits(newPeriod); isSuccess && !hideChart && fetchLogHits(newPeriod);
}).catch(e => e); }).catch(e => e);
setSearchParamsFromKeys( { setSearchParamsFromKeys( {
query, query,
@ -88,6 +91,10 @@ const ExploreLogs: FC = () => {
setTmpQuery(query); setTmpQuery(query);
}, [query]); }, [query]);
useEffect(() => {
!hideChart && fetchLogHits(period);
}, [hideChart]);
return ( return (
<div className="vm-explore-logs"> <div className="vm-explore-logs">
<ExploreLogsHeader <ExploreLogsHeader

View file

@ -10,6 +10,7 @@ import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart";
import Alert from "../../../components/Main/Alert/Alert"; import Alert from "../../../components/Main/Alert/Alert";
import { TimeParams } from "../../../types"; import { TimeParams } from "../../../types";
import LineLoader from "../../../components/Main/LineLoader/LineLoader"; import LineLoader from "../../../components/Main/LineLoader/LineLoader";
import { useSearchParams } from "react-router-dom";
import { getHitsTimeParams } from "../../../utils/logs"; import { getHitsTimeParams } from "../../../utils/logs";
interface Props { interface Props {
@ -24,6 +25,8 @@ interface Props {
const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onApplyFilter }) => { const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onApplyFilter }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const timeDispatch = useTimeDispatch(); const timeDispatch = useTimeDispatch();
const [searchParams] = useSearchParams();
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
const getYAxes = (logHits: LogHits[], timestamps: number[]) => { const getYAxes = (logHits: LogHits[], timestamps: number[]) => {
return logHits.map(hits => { return logHits.map(hits => {
@ -69,14 +72,16 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onA
const noData = data.every(d => d.length === 0); const noData = data.every(d => d.length === 0);
const noTimestamps = data[0].length === 0; const noTimestamps = data[0].length === 0;
const noValues = data[1].length === 0; const noValues = data[1].length === 0;
if (noData) { if (hideChart) {
return "Chart hidden. Hits updates paused.";
} else if (noData) {
return "No logs volume available\nNo volume information available for the current queries and time range."; return "No logs volume available\nNo volume information available for the current queries and time range.";
} else if (noTimestamps) { } else if (noTimestamps) {
return "No timestamp information available for the current queries and time range."; return "No timestamp information available for the current queries and time range.";
} else if (noValues) { } else if (noValues) {
return "No value information available for the current queries and time range."; return "No value information available for the current queries and time range.";
} return ""; } return "";
}, [data]); }, [data, hideChart]);
const setPeriod = ({ from, to }: {from: Date, to: Date}) => { const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
timeDispatch({ type: "SET_PERIOD", payload: { from, to } }); timeDispatch({ type: "SET_PERIOD", payload: { from, to } });

View file

@ -13,8 +13,12 @@
} }
&__empty { &__empty {
display: flex;
align-items: center;
justify-content: center;
position: absolute; position: absolute;
transform: translateY(-25px); top: 0;
bottom: 0;
z-index: 2; z-index: 2;
} }
} }

View file

@ -7,7 +7,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: $padding-global;
&-keys { &-keys {
max-height: 300px; max-height: 300px;

View file

@ -15,6 +15,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip ## tip
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add ability to hide hits chart. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7117).
* FEATURE: add basic [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vlogs.yml) for VictoriaLogs process. See details at [monitoring docs](https://docs.victoriametrics.com/victorialogs/#monitoring). * FEATURE: add basic [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vlogs.yml) for VictoriaLogs process. See details at [monitoring docs](https://docs.victoriametrics.com/victorialogs/#monitoring).
* FEATURE: improve [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe) performance on systems with many CPU cores when `by(...)` fields contain big number of unique values. For example, `_time:1d | stats by (user_id) count() x` should be executed much faster when `user_id` field contains millions of unique values. * FEATURE: improve [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe) performance on systems with many CPU cores when `by(...)` fields contain big number of unique values. For example, `_time:1d | stats by (user_id) count() x` should be executed much faster when `user_id` field contains millions of unique values.
* FEATURE: improve performance for [`top`](https://docs.victoriametrics.com/victorialogs/logsql/#top-pipe), [`uniq`](https://docs.victoriametrics.com/victorialogs/logsql/#uniq-pipe) and [`field_values`](https://docs.victoriametrics.com/victorialogs/logsql/#field_values-pipe) pipes on systems with many CPU cores when it is applied to [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with big number of unique values. For example, `_time:1d | top 5 (user_id)` should be executed much faster when `user_id` field contains millions of unique values. * FEATURE: improve performance for [`top`](https://docs.victoriametrics.com/victorialogs/logsql/#top-pipe), [`uniq`](https://docs.victoriametrics.com/victorialogs/logsql/#uniq-pipe) and [`field_values`](https://docs.victoriametrics.com/victorialogs/logsql/#field_values-pipe) pipes on systems with many CPU cores when it is applied to [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with big number of unique values. For example, `_time:1d | top 5 (user_id)` should be executed much faster when `user_id` field contains millions of unique values.