vmui/logs: improve graph usability (#7025)

### Describe Your Changes

- Show the time range in the tooltip when hovering over staircase
graphs.
- Use bolder lines for staircase graphs.
- Increase the number of steps on the staircase graph to 100.
- Reduce the maximum width of the tooltip to 1/3 of the screen.
- Insert only the label name under the cursor into the query input field
when `Ctrl`-clicking the line legend.

See [this
comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6545#issuecomment-2336805237).

### 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-09-27 13:19:46 +02:00 committed by GitHub
parent 09b309a82e
commit 8657d03433
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 73 additions and 21 deletions

View file

@ -6,6 +6,7 @@ import classNames from "classnames";
import { MouseEvent } from "react";
import { isMacOs } from "../../../../utils/detect-device";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { getStreamPairs } from "../../../../utils/logs";
interface Props {
uPlotInst: uPlot;
@ -14,20 +15,26 @@ interface Props {
const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
const [series, setSeries] = useState<Series[]>([]);
const [pairs, setPairs] = useState<string[][]>([]);
const updateSeries = useCallback(() => {
const series = uPlotInst.series.filter(s => s.scale !== "x");
setSeries(series);
setPairs(series.map(s => getStreamPairs(s.label || "")));
}, [uPlotInst]);
const handleClick = (target: Series) => (e: MouseEvent<HTMLDivElement>) => {
const handleClickByValue = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
const metaKey = e.metaKey || e.ctrlKey;
if (!metaKey) {
target.show = !target.show;
} else {
onApplyFilter(target.label || "");
}
if (!metaKey) return;
onApplyFilter(`{${value}}` || "");
updateSeries();
uPlotInst.redraw();
};
const handleClickByStream = (target: Series) => (e: MouseEvent<HTMLDivElement>) => {
const metaKey = e.metaKey || e.ctrlKey;
if (metaKey) return;
target.show = !target.show;
updateSeries();
uPlotInst.redraw();
};
@ -36,7 +43,7 @@ const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
return (
<div className="vm-bar-hits-legend">
{series.map(s => (
{series.map((s, i) => (
<Tooltip
key={s.label}
title={(
@ -51,13 +58,23 @@ const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
"vm-bar-hits-legend-item": true,
"vm-bar-hits-legend-item_hide": !s.show,
})}
onClick={handleClick(s)}
onClick={handleClickByStream(s)}
>
<div
className="vm-bar-hits-legend-item__marker"
style={{ backgroundColor: `${(s?.stroke as () => string)?.()}` }}
/>
<div>{s.label}</div>
<div className="vm-bar-hits-legend-item-pairs">
{pairs[i].map(value => (
<span
className="vm-bar-hits-legend-item-pairs__value"
key={value}
onClick={handleClickByValue(value)}
>
{value}
</span>
))}
</div>
</div>
</Tooltip>
))}

View file

@ -3,16 +3,16 @@
.vm-bar-hits-legend {
display: flex;
flex-wrap: wrap;
gap: 0;
gap: $padding-small;
padding: 0 $padding-small $padding-small;
&-item {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 4px;
gap: $padding-small;
font-size: 12px;
padding: $padding-small;
padding: 0 $padding-small;
border-radius: $border-radius-small;
cursor: pointer;
transition: 0.2s;
@ -31,5 +31,30 @@
height: 14px;
border: $color-background-block;
}
&-pairs {
display: flex;
gap: $padding-small;
&__value {
padding: $padding-small 0;
&:hover {
text-decoration: underline;
}
&:after {
content: ",";
}
&:last-child:after {
content: "";
}
}
}
}
&-info {
list-style-position: inside;
}
}

View file

@ -12,12 +12,16 @@ interface Props {
focusDataIdx: number;
}
const timeFormat = (ts: number) => dayjs(ts * 1000).tz().format(DATE_TIME_FORMAT);
const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
const tooltipRef = useRef<HTMLDivElement>(null);
const tooltipData = useMemo(() => {
const series = uPlotInst?.series || [];
const [time, ...values] = data.map((d) => d[focusDataIdx] || 0);
const step = (data[0][1] - data[0][0]);
const timeNext = time + step;
const tooltipItems = values.map((value, i) => {
const targetSeries = series[i + 1];
@ -41,7 +45,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
point,
values: tooltipItems,
total: tooltipItems.reduce((acc, item) => acc + item.value, 0),
timestamp: dayjs(time * 1000).tz().format(DATE_TIME_FORMAT),
timestamp: `${timeFormat(time)} - ${timeFormat(timeNext)}`,
};
}, [focusDataIdx, uPlotInst, data]);

View file

@ -19,8 +19,8 @@ const seriesColors = [
];
const strokeWidth = {
[GRAPH_STYLES.BAR]: 0.8,
[GRAPH_STYLES.LINE_STEPPED]: 1.2,
[GRAPH_STYLES.BAR]: 1,
[GRAPH_STYLES.LINE_STEPPED]: 2,
[GRAPH_STYLES.LINE]: 1.2,
[GRAPH_STYLES.POINTS]: 0,
};
@ -82,7 +82,7 @@ const useBarHitsOptions = ({
cursor: {
points: {
width: (u, seriesIdx, size) => size / 4,
size: (u, seriesIdx) => (u.series?.[seriesIdx]?.points?.size || 1) * 2.5,
size: (u, seriesIdx) => (u.series?.[seriesIdx]?.points?.size || 1) * 1.5,
stroke: (u, seriesIdx) => `${series?.[seriesIdx]?.stroke || "#ffffff"}`,
fill: () => "#ffffff",
},

View file

@ -29,6 +29,7 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
white-space: pre-wrap;
word-break: break-all;
width: auto;
max-width: calc(100vw/3);
}
&_sticky {

View file

@ -1,2 +1,2 @@
export const LOGS_ENTRIES_LIMIT = 50;
export const LOGS_BARS_VIEW = 20;
export const LOGS_BARS_VIEW = 100;

View file

@ -16,6 +16,7 @@ import TextField from "../../../components/Main/TextField/TextField";
import useBoolean from "../../../hooks/useBoolean";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import { useSearchParams } from "react-router-dom";
import { getStreamPairs } from "../../../utils/logs";
const WITHOUT_GROUPING = "No Grouping";
@ -62,12 +63,10 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
const groupData = useMemo(() => {
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
const streamValue = item.values[0]?.[groupBy] || "";
const pairs = /^{.+}$/.test(streamValue)
? streamValue.slice(1, -1).match(/(\\.|[^,])+/g) || [streamValue]
: [streamValue];
const pairs = getStreamPairs(streamValue);
return {
...item,
pairs: pairs.filter(Boolean),
pairs,
};
});
}, [logs, groupBy]);

View file

@ -0,0 +1,4 @@
export const getStreamPairs = (value: string): string[] => {
const pairs = /^{.+}$/.test(value) ? value.slice(1, -1).split(",") : [value];
return pairs.filter(Boolean);
};

View file

@ -15,6 +15,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): improved readability of staircase graphs and tooltip usability. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6545#issuecomment-2336805237).
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): simplify query input by adding only the label name when `ctrl`+clicking the line legend. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6545#issuecomment-2336805237).
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): keep selected columns in table view on page reloads. Before, selected columns were reset on each update. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7016).
* FEATURE: allow skipping `_stream:` prefix in [stream filters](https://docs.victoriametrics.com/victorialogs/logsql/#stream-filter). This simplifies writing queries with stream filters. Now `{foo="bar"}` is the recommended format for stream filters over the `_stream:{foo="bar"}` format.
* FEATURE: allow using `-` instead of `!` as `NOT` operator shorthand in [logical filters](https://docs.victoriametrics.com/victorialogs/logsql/#logical-filter). For example, `-info -warn` query is equivalent to `!info !warn`. This simplifies transition from other query languages with full-text search support, which usually use `-` as `NOT` operator.