mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-12 04:32:00 +01:00
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:
parent
1a6313ca68
commit
64793ff5f0
@ -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>
|
||||
))}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -1,2 +1,2 @@
|
||||
export const LOGS_ENTRIES_LIMIT = 50;
|
||||
export const LOGS_BARS_VIEW = 20;
|
||||
export const LOGS_BARS_VIEW = 100;
|
||||
|
@ -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]);
|
||||
|
4
app/vmui/packages/vmui/src/utils/logs.ts
Normal file
4
app/vmui/packages/vmui/src/utils/logs.ts
Normal 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);
|
||||
};
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user