vmui: maximum queries (#3196)

vmui: allow using up 4 queries at the same time

The change also introduces UI updates to make using 
multiple queries more conveniently.
This commit is contained in:
Yury Molodov 2022-10-06 07:10:21 +02:00 committed by GitHub
parent cdf385f9e4
commit a54987f671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 67 additions and 66 deletions

View File

@ -46,7 +46,7 @@ const AdditionalSettings: FC = () => {
control={<BasicSwitch checked={isTracingEnabled} onChange={onChangeQueryTracing} />}
/>
</Box>
<Box ml={2}>
<Box ml={2} mr={2}>
<StepConfigurator defaultStep={step} customStepEnable={customStep.enable}
setStep={(value) => {
graphDispatch({type: "SET_CUSTOM_STEP", payload: value});

View File

@ -4,17 +4,21 @@ import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import QueryEditor from "./QueryEditor";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import DeleteIcon from "@mui/icons-material/Delete";
import AddIcon from "@mui/icons-material/Add";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import AdditionalSettings from "./AdditionalSettings";
import {ErrorTypes} from "../../../../types";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
export interface QueryConfiguratorProps {
error?: ErrorTypes | string;
queryOptions: string[]
}
export const MAX_QUERY_FIELDS = 4;
const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) => {
const {query, queryHistory, queryControls: {autocomplete}} = useAppState();
@ -65,31 +69,31 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) =>
return <Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
<Box>
{stateQuery.map((q, i) =>
<Box key={i} display="grid" gridTemplateColumns="1fr auto auto" gap="4px" width="100%"
mb={i === stateQuery.length - 1 ? 0 : 2.5}>
<Box key={i} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%" position="relative"
mb={i === stateQuery.length - 1 ? 0 : 2}>
<QueryEditor
query={stateQuery[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions}
error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}
label={`Query ${i + 1}`}/>
{i === 0 && <Tooltip title="Execute Query">
<IconButton onClick={onRunQuery} sx={{height: "49px", width: "49px"}}>
<PlayCircleOutlineIcon/>
</IconButton>
</Tooltip>}
{stateQuery.length < 2 && <Tooltip title="Add Query">
<IconButton onClick={onAddQuery} sx={{height: "49px", width: "49px"}}>
<AddCircleOutlineIcon/>
</IconButton>
</Tooltip>}
{i > 0 && <Tooltip title="Remove Query">
<IconButton onClick={() => onRemoveQuery(i)} sx={{height: "49px", width: "49px"}}>
<HighlightOffIcon/>
label={`Query ${i + 1}`} size={"small"}/>
{stateQuery.length > 1 && <Tooltip title="Remove Query">
<IconButton onClick={() => onRemoveQuery(i)} sx={{height: "33px", width: "33px", padding: 0}} color={"error"}>
<DeleteIcon fontSize={"small"}/>
</IconButton>
</Tooltip>}
</Box>)}
</Box>
<Box mt={3}>
<Box mt={3} display="grid" gridTemplateColumns="1fr auto" alignItems="center">
<AdditionalSettings/>
<Box>
{stateQuery.length < MAX_QUERY_FIELDS && (
<Button variant="outlined" onClick={onAddQuery} startIcon={<AddIcon/>} sx={{mr: 2}}>
<Typography lineHeight={"20px"} fontWeight="500">Add Query</Typography>
</Button>
)}
<Button variant="contained" onClick={onRunQuery} startIcon={<PlayArrowIcon/>}>
<Typography lineHeight={"20px"} fontWeight="500">Execute Query</Typography>
</Button>
</Box>
</Box>
</Box>;
};

View File

@ -20,6 +20,7 @@ export interface QueryEditorProps {
error?: ErrorTypes | string;
queryOptions: string[];
label: string;
size?: "small" | "medium" | undefined;
}
const QueryEditor: FC<QueryEditorProps> = ({
@ -32,6 +33,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
error,
queryOptions,
label,
size = "medium"
}) => {
const [focusField, setFocusField] = useState(false);
@ -112,6 +114,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
onFocus={() => setFocusField(true)}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value, index)}
size={size}
/>
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start" sx={{zIndex: 3}}>
<ClickAwayListener onClickAway={() => setOpenAutocomplete(false)}>

View File

@ -66,7 +66,7 @@ const CustomPanel: FC = () => {
<Box height="100%">
{isLoading && <Spinner isLoading={isLoading} height={"500px"}/>}
{<Box height={"100%"} bgcolor={"#fff"}>
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mb={2}
borderBottom={1} borderColor="divider">
<DisplayTypeSwitch/>
<Box display={"flex"}>

View File

@ -1,8 +1,7 @@
import React, {FC, useMemo, useState} from "preact/compat";
import {hexToRGB} from "../../utils/color";
import {LegendItem} from "../../utils/uplot/types";
import "./legend.css";
import {getDashLine} from "../../utils/uplot/helpers";
import {getLegendLabel} from "../../utils/uplot/helpers";
import Tooltip from "@mui/material/Tooltip";
export interface LegendProps {
@ -28,31 +27,22 @@ const Legend: FC<LegendProps> = ({labels, query, onChange}) => {
<div className="legendWrapper">
{groups.map((group) => <div className="legendGroup" key={group}>
<div className="legendGroupTitle">
<svg className="legendGroupLine" width="33" height="3" version="1.1" xmlns="http://www.w3.org/2000/svg">
<line strokeWidth="3" x1="0" y1="0" x2="33" y2="0" stroke="#363636"
strokeDasharray={getDashLine(group).join(",")}
/>
</svg>
<span className="legendGroupQuery">Query {group}</span>
<span>(&quot;{query[group - 1]}&quot;)</span>
</div>
<div>
{labels.filter(l => l.group === group).map((legendItem: LegendItem) =>
<div className={legendItem.checked ? "legendItem" : "legendItem legendItemHide"}
key={`${legendItem.group}.${legendItem.label}`}
key={legendItem.label}
onClick={(e) => onChange(legendItem, e.ctrlKey || e.metaKey)}>
<div className="legendMarker"
style={{
borderColor: legendItem.color,
backgroundColor: `rgba(${hexToRGB(legendItem.color)}, 0.1)`
}}/>
<div className="legendMarker" style={{backgroundColor: legendItem.color}}/>
<div className="legendLabel">
{legendItem.label.replace(/{.+}/gmi, "")}
{getLegendLabel(legendItem.label)}
{!!Object.keys(legendItem.freeFormFields).length && <>
&#160;&#123;
{Object.keys(legendItem.freeFormFields).filter(f => f !== "__name__").map((f) => {
const freeField = `${f}="${legendItem.freeFormFields[f]}"`;
const fieldId = `${legendItem.group}.${legendItem.label}.${freeField}`;
const fieldId = `${legendItem.label}.${freeField}`;
return <Tooltip arrow key={f} open={copiedValue === fieldId} title={"Copied!"}>
<span className="legendFreeFields" onClick={(e) => {
e.stopPropagation();

View File

@ -53,8 +53,6 @@
.legendMarker {
width: 12px;
height: 12px;
border-width: 2px;
border-style: solid;
box-sizing: border-box;
transition: 0.2s ease;
}

View File

@ -130,7 +130,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [],
const options: uPlotOptions = {
...defaultOptions,
series,
axes: getAxes(series.length > 1 ? series : [{}, {scale: "1"}], unit),
axes: getAxes( [{}, {scale: "1"}], unit),
scales: {...getScales()},
width: layoutSize.width || 400,
plugins: [{hooks: {ready: onReadyChart, setCursor, setSeries: seriesFocus}}],

View File

@ -1,6 +1,7 @@
import qs from "qs";
import get from "lodash.get";
import router from "../router";
import {MAX_QUERY_FIELDS} from "../components/CustomPanel/Configurator/Query/QueryConfigurator";
const graphStateToUrlParams = {
"time.duration": "range_input",
@ -105,7 +106,7 @@ export const getQueryStringValue = (
export const getQueryArray = (): string[] => {
const queryLength = window.location.search.match(/g\d+.expr/gmi)?.length || 1;
return new Array(queryLength).fill(1).map((q, i) => {
return getQueryStringValue(`g${i}.expr`, "") as string;
});
return new Array(queryLength > MAX_QUERY_FIELDS ? MAX_QUERY_FIELDS : queryLength)
.fill(1)
.map((q, i) => getQueryStringValue(`g${i}.expr`, "") as string);
};

View File

@ -48,16 +48,15 @@ export const getMinMaxBuffer = (min: number | null, max: number | null): [number
}
const valueRange = Math.abs(max - min) || Math.abs(min) || 1;
const padding = 0.02*valueRange;
return [min - padding, max + padding];
return [Math.floor(min - padding), Math.ceil(max + padding)];
};
export const getLimitsYAxis = (values: { [key: string]: number[] }): AxisRange => {
const result: AxisRange = {};
for (const key in values) {
const numbers = values[key];
const min = getMinFromArray(numbers);
const max = getMaxFromArray(numbers);
result[key] = getMinMaxBuffer(min, max);
}
const numbers = Object.values(values).flat();
const key = "1";
const min = getMinFromArray(numbers);
const max = getMaxFromArray(numbers);
result[key] = getMinMaxBuffer(min, max);
return result;
};

View File

@ -63,6 +63,10 @@ export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum:
return Math.ceil(axisSize);
};
export const getColorLine = (scale: number, label: string): string => getColorFromString(`${scale}${label}`);
export const getColorLine = (label: string): string => getColorFromString(label);
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
export const getLegendLabel = (label: string): string => {
return label.replace(/^\[\d+]/, "").replace(/{.+}/gmi, "");
};

View File

@ -2,7 +2,7 @@ import {MetricResult} from "../../api/types";
import {Series} from "uplot";
import {getNameForMetric} from "../metric";
import {BarSeriesItem, Disp, Fill, LegendItem, Stroke} from "./types";
import {getColorLine, getDashLine} from "./helpers";
import {getColorLine} from "./helpers";
import {HideSeriesArgs} from "./types";
interface SeriesItem extends Series {
@ -10,15 +10,15 @@ interface SeriesItem extends Series {
}
export const getSeriesItem = (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
const label = getNameForMetric(d, alias[d.group - 1]);
const name = getNameForMetric(d, alias[d.group - 1]);
const label = `[${d.group}]${name}`;
return {
label,
dash: getDashLine(d.group),
freeFormFields: d.metric,
width: 1.4,
stroke: getColorLine(d.group, label),
show: !includesHideSeries(label, d.group, hideSeries),
scale: String(d.group),
stroke: getColorLine(label),
show: !includesHideSeries(label, hideSeries),
scale: "1",
points: {
size: 4.2,
width: 1.4
@ -35,9 +35,9 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItem => ({
});
export const getHideSeries = ({hideSeries, legend, metaKey, series}: HideSeriesArgs): string[] => {
const label = `${legend.group}.${legend.label}`;
const include = includesHideSeries(legend.label, legend.group, hideSeries);
const labels = series.map(s => `${s.scale}.${s.label}`);
const {label} = legend;
const include = includesHideSeries(label, hideSeries);
const labels = series.map(s => s.label || "");
if (metaKey) {
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
} else if (hideSeries.length) {
@ -47,8 +47,8 @@ export const getHideSeries = ({hideSeries, legend, metaKey, series}: HideSeriesA
}
};
export const includesHideSeries = (label: string, group: string | number, hideSeries: string[]): boolean => {
return hideSeries.includes(`${group}.${label}`);
export const includesHideSeries = (label: string, hideSeries: string[]): boolean => {
return hideSeries.includes(`${label}`);
};
export const getBarSeries = (

View File

@ -1,6 +1,6 @@
import dayjs from "dayjs";
import {SetupTooltip} from "./types";
import {getColorLine, formatPrettyNumber} from "./helpers";
import {getColorLine, formatPrettyNumber, getLegendLabel} from "./helpers";
export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit = ""}: SetupTooltip): void => {
const {seriesIdx, dataIdx} = tooltipIdx;
@ -9,7 +9,7 @@ export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffs
const dataTime = u.data[0][dataIdx];
const metric = metrics[seriesIdx - 1]?.metric || {};
const selectedSeries = series[seriesIdx];
const color = getColorLine(Number(selectedSeries.scale || 0), selectedSeries.label || "");
const color = getColorLine(selectedSeries.label || "");
const {width, height} = u.over.getBoundingClientRect();
const top = u.valToPos((dataSeries || 0), series[seriesIdx]?.scale || "1");
@ -22,8 +22,7 @@ export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffs
tooltip.style.top = `${tooltipOffset.top + top + 10 - (overflowY ? tooltipHeight + 10 : 0)}px`;
tooltip.style.left = `${tooltipOffset.left + lft + 10 - (overflowX ? tooltipWidth + 20 : 0)}px`;
const metricName = (selectedSeries.label || "").replace(/{.+}/gmi, "").trim();
const groupName = `Query ${selectedSeries.scale}`;
const name = metricName || groupName;
const name = getLegendLabel(metricName);
const date = dayjs(new Date(dataTime * 1000)).format("YYYY-MM-DD HH:mm:ss:SSS (Z)");
const info = Object.keys(metric).filter(k => k !== "__name__").map(k => `<div><b>${k}</b>: ${metric[k]}</div>`).join("");
const marker = `<div class="u-tooltip__marker" style="background: ${color}"></div>`;

View File

@ -37,6 +37,8 @@ See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#m
* FEATURE: allow to define the minimum TLS version to use when accepting https requests to VictoriaMetrics components if `-tls` command-line flag is set. The minimum TLS version can be set via `-tlsMinVersion` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3090).
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add `vm-native-step-interval` command line flag for `vm-native` mode. New option allows splitting the import process into chunks by time interval. This helps migrating data sets with high churn rate and provides better control over the process. See [feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2733).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add `top queries` tab, which shows various stats for recently executed queries. See [these docs](https://docs.victoriametrics.com/#top-queries) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2707).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): move the "Execute Query" and "Add Query" buttons below the query fields, change icon for remove query. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3101).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): set the maximum number of queries to 4, remove multi Y-axes, left one for all queries and dotted lines to indicate queries in the graph. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3169).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `debug` mode to the alerting rule settings for printing additional information into logs during evaluation. See `debug` param in [alerting rule config](https://docs.victoriametrics.com/vmalert.html#alerting-rules).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add experimental feature for displaying last 10 states of the rule (recording or alerting) evaluation. The state is available on the Rule page, which can be opened by clicking on `Details` link next to Rule's name on the `/groups` page.
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow using extra labels in annotiations. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3013).
@ -64,6 +66,7 @@ See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#m
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly calculate query results at `vmselect`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3067). The issue has been introduced in [v1.81.0](https://docs.victoriametrics.com/CHANGELOG.html#v1810).
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): log clear error when multiple identical `-storageNode` command-line flags are passed to `vmselect` or to `vminsert`. Previously these components were crashed with cryptic panic `metric ... is already registered` in this case. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3076).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix `RangeError: Maximum call stack size exceeded` error when the query returns too many data points at `Table` view. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3092/files).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix workaround for adding more queries via URL. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3169).
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): re-evaluate annotations per each alert evaluation. Previously, annotations were evaluated only on alert's value change. This could result in stale annotations in some cases described in [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3119).
* BUGFIX: prevent from excessive CPU usage when the storage enters [read-only mode](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#readonly-mode). The previous fix in [v1.81.0](https://docs.victoriametrics.com/CHANGELOG.html#v1810) wasn't complete.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): change default value for command-line flag `-datasource.queryStep` from `0s` to `5m`. Param `step` is added by vmalert to every rule evaluation request sent to datasource. Before this change, `step` was equal to group's evaluation interval by default. Param `step` for instant queries defines how far VM can look back for the last written data point. The change supposed to improve reliability of the rules evaluation when evaluation interval is lower than scraping interval.