vmui: autocomplete usability improvements (#5422)

* vmui: add show quick tip for autocomplete

* vmui: auto-completion usability improvements #5348

* vmui: add const for min symbols in autocomplete

* Use proper queries to VictoriaMetrics

* vmui: fix comments for autocomplete

* app/vmselect: run `make vmui-update`

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-12-12 23:32:41 +01:00 committed by GitHub
parent 0f91f83639
commit 1a5cdb4790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 400 additions and 167 deletions

View File

@ -396,6 +396,9 @@ The UI allows exploring query results via graphs and tables. It also provides th
- [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work; - [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work;
- [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs. - [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs.
VMUI provides auto-completion for [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html) functions, metric names, label names and label values. The auto-completion can be enabled
by checking the `Autocomplete` toggle. When the auto-completion is disabled, it can still be triggered for the current cursor position by pressing `ctrl+space`.
VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets
(both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram) (both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram)
and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported). and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported).

View File

@ -1,13 +1,13 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.349e6522.css", "main.css": "./static/css/main.fb353c1e.css",
"main.js": "./static/js/main.c93073e5.js", "main.js": "./static/js/main.5bcddddc.js",
"static/js/522.da77e7b3.chunk.js": "./static/js/522.da77e7b3.chunk.js", "static/js/522.da77e7b3.chunk.js": "./static/js/522.da77e7b3.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.8644fd7c964802dd34a9.md", "static/media/MetricsQL.md": "./static/media/MetricsQL.b64c4dbf91f4fa581621.md",
"index.html": "./index.html" "index.html": "./index.html"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.349e6522.css", "static/css/main.fb353c1e.css",
"static/js/main.c93073e5.js" "static/js/main.5bcddddc.js"
] ]
} }

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.c93073e5.js"></script><link href="./static/css/main.349e6522.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.5bcddddc.js"></script><link href="./static/css/main.fb353c1e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1029,7 +1029,7 @@ for every point of every time series returned by `q`.
Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names. Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is supported by PromQL. This function is supported by PromQL. See also [acosh](#acosh). This function is supported by PromQL. See also [acosh](#acosh).
#### day_of_month #### day_of_month
@ -1049,6 +1049,15 @@ Metric names are stripped from the resulting series. Add [keep_metric_names](#ke
This function is supported by PromQL. This function is supported by PromQL.
#### day_of_year
`day_of_year(q)` is a [transform function](#transform-functions), which returns the day of year for every point of every time series returned by `q`.
It is expected that `q` returns unix timestamps. The returned values are in the range `[1...365]` for non-leap years, and `[1 to 366]` in leap years.
Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is supported by PromQL.
#### days_in_month #### days_in_month
`days_in_month(q)` is a [transform function](#transform-functions), which returns the number of days in the month identified `days_in_month(q)` is a [transform function](#transform-functions), which returns the number of days in the month identified

View File

@ -1029,7 +1029,7 @@ for every point of every time series returned by `q`.
Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names. Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is supported by PromQL. This function is supported by PromQL. See also [acosh](#acosh). This function is supported by PromQL. See also [acosh](#acosh).
#### day_of_month #### day_of_month
@ -1049,6 +1049,15 @@ Metric names are stripped from the resulting series. Add [keep_metric_names](#ke
This function is supported by PromQL. This function is supported by PromQL.
#### day_of_year
`day_of_year(q)` is a [transform function](#transform-functions), which returns the day of year for every point of every time series returned by `q`.
It is expected that `q` returns unix timestamps. The returned values are in the range `[1...365]` for non-leap years, and `[1 to 366]` in leap years.
Metric names are stripped from the resulting series. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is supported by PromQL.
#### days_in_month #### days_in_month
`days_in_month(q)` is a [transform function](#transform-functions), which returns the number of days in the month identified `days_in_month(q)` is a [transform function](#transform-functions), which returns the number of days in the month identified

View File

@ -11,7 +11,7 @@ import classNames from "classnames";
import useBoolean from "../../../hooks/useBoolean"; import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener"; import useEventListener from "../../../hooks/useEventListener";
import Tooltip from "../../Main/Tooltip/Tooltip"; import Tooltip from "../../Main/Tooltip/Tooltip";
import { AUTOCOMPLETE_KEY } from "../../Main/ShortcutKeys/constants/keyList"; import { AUTOCOMPLETE_QUICK_KEY } from "../../Main/ShortcutKeys/constants/keyList";
const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => { const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
const { autocomplete } = useQueryState(); const { autocomplete } = useQueryState();
@ -32,11 +32,16 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" }); queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
}; };
const onChangeQuickAutocomplete = () => {
queryDispatch({ type: "SET_AUTOCOMPLETE_QUICK", payload: true });
};
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
const { code, altKey } = e; /** @see AUTOCOMPLETE_QUICK_KEY */
if (code === "KeyA" && altKey) { const { code, ctrlKey, altKey } = e;
if (code === "Space" && (ctrlKey || altKey)) {
e.preventDefault(); e.preventDefault();
onChangeAutocomplete(); onChangeQuickAutocomplete();
} }
}; };
@ -49,7 +54,7 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
"vm-additional-settings_mobile": isMobile "vm-additional-settings_mobile": isMobile
})} })}
> >
<Tooltip title={AUTOCOMPLETE_KEY}> <Tooltip title={<>Quick tip: {AUTOCOMPLETE_QUICK_KEY}</>}>
<Switch <Switch
label={"Autocomplete"} label={"Autocomplete"}
value={autocomplete} value={autocomplete}

View File

@ -0,0 +1,43 @@
import { AUTOCOMPLETE_LIMITS } from "../../../constants/queryAutocomplete";
export type QueryAutocompleteCacheItem = {
type: string;
value: string;
start: string;
end: string;
match: string;
}
export class QueryAutocompleteCache {
private maxSize: number;
private map: Map<string, string[]>;
constructor() {
this.maxSize = AUTOCOMPLETE_LIMITS.cacheLimit;
this.map = new Map();
}
get(key: QueryAutocompleteCacheItem) {
for (const [cacheKey, cacheValue] of this.map) {
const cacheItem = JSON.parse(cacheKey) as QueryAutocompleteCacheItem;
const equalRange = cacheItem.start === key.start && cacheItem.end === key.end;
const equalType = cacheItem.type === key.type;
const isIncluded = key.value && cacheItem.value && key.value.includes(cacheItem.value);
const isSimilar = cacheItem.match === key.match || isIncluded;
const isUnderLimit = cacheValue.length < AUTOCOMPLETE_LIMITS.queryLimit;
if (isSimilar && equalRange && equalType && isUnderLimit) {
return cacheValue;
}
}
return this.map.get(JSON.stringify(key));
}
put(key: QueryAutocompleteCacheItem, value: string[]) {
if (this.map.size >= this.maxSize) {
const firstKey = this.map.keys().next().value;
this.map.delete(firstKey);
}
this.map.set(JSON.stringify(key), value);
}
}

View File

@ -1,5 +1,5 @@
import React, { FC, useRef, useState } from "preact/compat"; import React, { FC, useRef, useState } from "preact/compat";
import { KeyboardEvent } from "react"; import { KeyboardEvent, useEffect } from "react";
import { ErrorTypes } from "../../../types"; import { ErrorTypes } from "../../../types";
import TextField from "../../Main/TextField/TextField"; import TextField from "../../Main/TextField/TextField";
import QueryEditorAutocomplete from "./QueryEditorAutocomplete"; import QueryEditorAutocomplete from "./QueryEditorAutocomplete";
@ -7,6 +7,7 @@ import "./style.scss";
import { QueryStats } from "../../../api/types"; import { QueryStats } from "../../../api/types";
import { partialWarning, seriesFetchedWarning } from "./warningText"; import { partialWarning, seriesFetchedWarning } from "./warningText";
import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete"; import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
import { useQueryDispatch } from "../../../state/query/QueryStateContext";
export interface QueryEditorProps { export interface QueryEditorProps {
onChange: (query: string) => void; onChange: (query: string) => void;
@ -38,6 +39,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
const [openAutocomplete, setOpenAutocomplete] = useState(false); const [openAutocomplete, setOpenAutocomplete] = useState(false);
const [caretPosition, setCaretPosition] = useState([0, 0]); const [caretPosition, setCaretPosition] = useState([0, 0]);
const autocompleteAnchorEl = useRef<HTMLInputElement>(null); const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
const queryDispatch = useQueryDispatch();
const warning = [ const warning = [
{ {
@ -100,6 +102,10 @@ const QueryEditor: FC<QueryEditorProps> = ({
setCaretPosition(val); setCaretPosition(val);
}; };
useEffect(() => {
queryDispatch({ type: "SET_AUTOCOMPLETE_QUICK", payload: false });
}, [value]);
return ( return (
<div <div
className="vm-query-editor" className="vm-query-editor"

View File

@ -2,15 +2,11 @@ import React, { FC, Ref, useState, useEffect, useMemo } from "preact/compat";
import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete"; import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions"; import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
import { getTextWidth } from "../../../utils/uplot"; import { getTextWidth } from "../../../utils/uplot";
import { escapeRegExp } from "../../../utils/regexp"; import { escapeRegexp } from "../../../utils/regexp";
import useGetMetricsQL from "../../../hooks/useGetMetricsQL"; import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
import { RefreshIcon } from "../../Main/Icons";
enum ContextType { import { QueryContextType } from "../../../types";
empty = "empty", import { AUTOCOMPLETE_LIMITS, AUTOCOMPLETE_MIN_SYMBOLS } from "../../../constants/queryAutocomplete";
metricsql = "metricsql",
label = "label",
value = "value",
}
interface QueryEditorAutocompleteProps { interface QueryEditorAutocompleteProps {
value: string; value: string;
@ -37,65 +33,70 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
}, [value]); }, [value]);
const label = useMemo(() => { const label = useMemo(() => {
const regexp = /[a-z_]\w*(?=\s*(=|!=|=~|!~))/g; const regexp = /[a-z_:-][\w\-.:/]*\b(?=\s*(=|!=|=~|!~))/g;
const match = value.match(regexp); const match = value.match(regexp);
return match ? match[match.length - 1] : ""; return match ? match[match.length - 1] : "";
}, [value]); }, [value]);
const metricRegexp = new RegExp(`\\(?(${escapeRegExp(metric)})$`, "g");
const labelRegexp = /[{.,].?(\w+)$/gm;
const valueRegexp = new RegExp(`(${escapeRegExp(metric)})?{?.+${escapeRegExp(label)}="?([^"]*)$`, "g");
const context = useMemo(() => { const context = useMemo(() => {
[metricRegexp, labelRegexp, valueRegexp].forEach(regexp => regexp.lastIndex = 0); if (!value) return QueryContextType.empty;
switch (true) {
case valueRegexp.test(value):
return ContextType.value;
case labelRegexp.test(value):
return ContextType.label;
case metricRegexp.test(value):
return ContextType.metricsql;
default:
return ContextType.empty;
}
}, [value, valueRegexp, labelRegexp, metricRegexp]);
const { metrics, labels, values } = useFetchQueryOptions({ metric, label }); const labelRegexp = /\{[^}]*?(\w+)$/gm;
const labelValueRegexp = new RegExp(`(${escapeRegexp(metric)})?{?.+${escapeRegexp(label)}(=|!=|=~|!~)"?([^"]*)$`, "g");
switch (true) {
case labelValueRegexp.test(value):
return QueryContextType.labelValue;
case labelRegexp.test(value):
return QueryContextType.label;
default:
return QueryContextType.metricsql;
}
}, [value, metric, label]);
const valueByContext = useMemo(() => {
const wordMatch = value.match(/([\w_\-.:/]+(?![},]))$/);
return wordMatch ? wordMatch[0] : "";
}, [value]);
const { metrics, labels, labelValues, loading } = useFetchQueryOptions({
valueByContext,
metric,
label,
context,
});
const options = useMemo(() => { const options = useMemo(() => {
switch (context) { switch (context) {
case ContextType.metricsql: case QueryContextType.metricsql:
return [...metrics, ...metricsqlFunctions]; return [...metrics, ...metricsqlFunctions];
case ContextType.label: case QueryContextType.label:
return labels; return labels;
case ContextType.value: case QueryContextType.labelValue:
return values; return labelValues;
default: default:
return []; return [];
} }
}, [context, metrics, labels, values]); }, [context, metrics, labels, labelValues]);
const valueByContext = useMemo(() => {
if (value.length !== caretPosition[1]) return value;
const wordMatch = value.match(/([\w_]+)$/) || [];
return wordMatch[1] || "";
}, [context, caretPosition, value]);
const handleSelect = (insert: string) => { const handleSelect = (insert: string) => {
const wordMatch = value.match(/([\w_]+)$/); // Find the start and end of valueByContext in the query string
const wordMatchIndex = wordMatch?.index !== undefined ? wordMatch.index : value.length; const startIndexOfValueByContext = value.lastIndexOf(valueByContext, caretPosition[0]);
const beforeInsert = value.substring(0, wordMatchIndex); const endIndexOfValueByContext = startIndexOfValueByContext + valueByContext.length;
const afterInsert = value.substring(wordMatchIndex + (wordMatch?.[1].length || 0));
if (context === ContextType.value) { // Split the original string into parts: before, during, and after valueByContext
const beforeValueByContext = value.substring(0, startIndexOfValueByContext);
const afterValueByContext = value.substring(endIndexOfValueByContext);
// Add quotes around the value if the context is labelValue
if (context === QueryContextType.labelValue) {
const quote = "\""; const quote = "\"";
const needsQuote = beforeInsert[beforeInsert.length - 1] !== quote; const needsQuote = !beforeValueByContext.endsWith(quote);
insert = `${needsQuote ? quote : ""}${insert}${quote}`; insert = `${needsQuote ? quote : ""}${insert}${quote}`;
} }
const newVal = `${beforeInsert}${insert}${afterInsert}`; // Assemble the new value with the inserted text
const newVal = `${beforeValueByContext}${insert}${afterValueByContext}`;
onSelect(newVal); onSelect(newVal);
}; };
@ -113,16 +114,23 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
}, [anchorEl, caretPosition]); }, [anchorEl, caretPosition]);
return ( return (
<Autocomplete <>
disabledFullScreen <Autocomplete
value={valueByContext} disabledFullScreen
options={options} value={valueByContext}
anchor={anchorEl} options={options?.length < AUTOCOMPLETE_LIMITS.queryLimit ? options : []}
minLength={context === ContextType.metricsql ? 2 : 0} anchor={anchorEl}
offset={{ top: 0, left: leftOffset }} minLength={AUTOCOMPLETE_MIN_SYMBOLS[context]}
onSelect={handleSelect} offset={{ top: 0, left: leftOffset }}
onFoundOptions={onFoundOptions} onSelect={handleSelect}
/> onFoundOptions={onFoundOptions}
maxDisplayResults={{
limit: AUTOCOMPLETE_LIMITS.displayResults,
message: "Please, specify the query more precisely."
}}
/>
{loading && <div className="vm-query-editor-autocomplete"><RefreshIcon/></div>}
</>
); );
}; };

View File

@ -4,7 +4,18 @@
position: relative; position: relative;
&-autocomplete { &-autocomplete {
max-height: 300px; position: absolute;
overflow: auto; display: flex;
align-items: center;
justify-items: center;
top: 0;
bottom: 0;
right: $padding-global;
width: 12px;
height: 100%;
color: $color-text-secondary;
z-index: 2;
animation: half-circle-spinner-animation 1s infinite linear, vm-fade 0.5s ease-in;
pointer-events: none;
} }
} }

View File

@ -26,6 +26,7 @@ interface AutocompleteProps {
label?: string label?: string
disabledFullScreen?: boolean disabledFullScreen?: boolean
offset?: {top: number, left: number} offset?: {top: number, left: number}
maxDisplayResults?: {limit: number, message?: string}
onSelect: (val: string) => void onSelect: (val: string) => void
onOpenAutocomplete?: (val: boolean) => void onOpenAutocomplete?: (val: boolean) => void
onFoundOptions?: (val: AutocompleteOptions[]) => void onFoundOptions?: (val: AutocompleteOptions[]) => void
@ -48,6 +49,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
label, label,
disabledFullScreen, disabledFullScreen,
offset, offset,
maxDisplayResults,
onSelect, onSelect,
onOpenAutocomplete, onOpenAutocomplete,
onFoundOptions onFoundOptions
@ -56,6 +58,8 @@ const Autocomplete: FC<AutocompleteProps> = ({
const wrapperEl = useRef<HTMLDivElement>(null); const wrapperEl = useRef<HTMLDivElement>(null);
const [focusOption, setFocusOption] = useState<{index: number, type?: FocusType}>({ index: -1 }); const [focusOption, setFocusOption] = useState<{index: number, type?: FocusType}>({ index: -1 });
const [showMessage, setShowMessage] = useState("");
const [totalFound, setTotalFound] = useState(0);
const { const {
value: openAutocomplete, value: openAutocomplete,
@ -68,7 +72,14 @@ const Autocomplete: FC<AutocompleteProps> = ({
try { try {
const regexp = new RegExp(String(value.trim()), "i"); const regexp = new RegExp(String(value.trim()), "i");
const found = options.filter((item) => regexp.test(item.value)); const found = options.filter((item) => regexp.test(item.value));
return found.sort((a,b) => (a.value.match(regexp)?.index || 0) - (b.value.match(regexp)?.index || 0)); const sorted = found.sort((a, b) => {
if (a.value.toLowerCase() === value.trim().toLowerCase()) return -1;
if (b.value.toLowerCase() === value.trim().toLowerCase()) return 1;
return (a.value.match(regexp)?.index || 0) - (b.value.match(regexp)?.index || 0);
});
setTotalFound(sorted.length);
setShowMessage(sorted.length > Number(maxDisplayResults?.limit) ? maxDisplayResults?.message || "" : "");
return maxDisplayResults?.limit ? sorted.slice(0, maxDisplayResults.limit) : sorted;
} catch (e) { } catch (e) {
return []; return [];
} }
@ -133,7 +144,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
useEffect(() => { useEffect(() => {
setOpenAutocomplete(value.length >= minLength); setOpenAutocomplete(value.length >= minLength);
}, [value]); }, [value, options]);
useEventListener("keydown", handleKeyDown); useEventListener("keydown", handleKeyDown);
@ -170,7 +181,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
ref={wrapperEl} ref={wrapperEl}
> >
{displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>} {displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>}
{foundOptions.map((option, i) => {!(foundOptions.length === 1 && foundOptions[0]?.value === value) && foundOptions.map((option, i) =>
<div <div
className={classNames({ className={classNames({
"vm-list-item": true, "vm-list-item": true,
@ -192,6 +203,11 @@ const Autocomplete: FC<AutocompleteProps> = ({
</div> </div>
)} )}
</div> </div>
{showMessage && (
<div className="vm-autocomplete-message">
Shown {maxDisplayResults?.limit} results out of {totalFound}. {showMessage}
</div>
)}
{foundOptions[focusOption.index]?.description && ( {foundOptions[focusOption.index]?.description && (
<div className="vm-autocomplete-info"> <div className="vm-autocomplete-info">
<div className="vm-autocomplete-info__type"> <div className="vm-autocomplete-info__type">

View File

@ -16,16 +16,33 @@
color: $color-text-disabled; color: $color-text-disabled;
} }
&-info { &-info,
position: absolute; &-message {
top: calc(100% + 1px);
left: 0;
right: 0;
min-width: 450px;
padding: $padding-global; padding: $padding-global;
background-color: $color-background-block; background-color: $color-background-block;
box-shadow: $box-shadow-popper; border-top: $border-divider;
border-radius: $border-radius-small; }
&-message {
position: relative;
color: $color-warning;
font-size: $font-size-small;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $color-warning;
opacity: 0.1;
}
}
&-info {
min-width: 450px;
max-width: 500px;
overflow-wrap: anywhere; overflow-wrap: anywhere;
&__type { &__type {

View File

@ -4,9 +4,8 @@ import { VisibilityIcon } from "../../Icons";
import GraphTips from "../../../Chart/GraphTips/GraphTips"; import GraphTips from "../../../Chart/GraphTips/GraphTips";
const ctrlMeta = <code>{isMacOs() ? "Cmd" : "Ctrl"}</code>; const ctrlMeta = <code>{isMacOs() ? "Cmd" : "Ctrl"}</code>;
const altMeta = <code>{isMacOs() ? "Option" : "Alt"}</code>;
export const AUTOCOMPLETE_KEY = <>{altMeta} + <code>A</code></>; export const AUTOCOMPLETE_QUICK_KEY = <>{<code>{isMacOs() ? "Option" : "Ctrl"}</code>} + <code>Space</code></>;
const keyList = [ const keyList = [
{ {
@ -33,8 +32,8 @@ const keyList = [
description: "Toggle multiple queries" description: "Toggle multiple queries"
}, },
{ {
keys: AUTOCOMPLETE_KEY, keys: AUTOCOMPLETE_QUICK_KEY,
description: "Toggle autocomplete" description: "Show quick autocomplete tips"
} }
] ]
}, },

View File

@ -83,7 +83,6 @@ const TextField: FC<TextFieldProps> = ({
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleKeyDown = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
onKeyDown && onKeyDown(e); onKeyDown && onKeyDown(e);
updateCaretPosition(e.currentTarget);
const { key, ctrlKey, metaKey } = e; const { key, ctrlKey, metaKey } = e;
const isEnter = key === "Enter"; const isEnter = key === "Enter";
const runByEnter = type !== "textarea" ? isEnter : isEnter && (metaKey || ctrlKey); const runByEnter = type !== "textarea" ? isEnter : isEnter && (metaKey || ctrlKey);
@ -93,6 +92,10 @@ const TextField: FC<TextFieldProps> = ({
} }
}; };
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
updateCaretPosition(e.currentTarget);
};
const handleChange = (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (disabled) return; if (disabled) return;
onChange && onChange(e.currentTarget.value); onChange && onChange(e.currentTarget.value);
@ -135,6 +138,7 @@ const TextField: FC<TextFieldProps> = ({
autoCapitalize={"none"} autoCapitalize={"none"}
onInput={handleChange} onInput={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
@ -152,6 +156,7 @@ const TextField: FC<TextFieldProps> = ({
autoCapitalize={"none"} autoCapitalize={"none"}
onInput={handleChange} onInput={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}

View File

@ -0,0 +1,14 @@
import { QueryContextType } from "../types";
export const AUTOCOMPLETE_LIMITS = {
displayResults: 50,
queryLimit: 1000,
cacheLimit: 1000,
};
export const AUTOCOMPLETE_MIN_SYMBOLS = {
[QueryContextType.metricsql]: 2,
[QueryContextType.empty]: 2,
[QueryContextType.label]: 0,
[QueryContextType.labelValue]: 0,
};

View File

@ -5,140 +5,176 @@ import { AutocompleteOptions } from "../components/Main/Autocomplete/Autocomplet
import { LabelIcon, MetricIcon, ValueIcon } from "../components/Main/Icons"; import { LabelIcon, MetricIcon, ValueIcon } from "../components/Main/Icons";
import { useTimeState } from "../state/time/TimeStateContext"; import { useTimeState } from "../state/time/TimeStateContext";
import { useCallback } from "react"; import { useCallback } from "react";
import qs from "qs"; import debounce from "lodash.debounce";
import dayjs from "dayjs"; import { useQueryDispatch, useQueryState } from "../state/query/QueryStateContext";
import { QueryContextType } from "../types";
import { AUTOCOMPLETE_LIMITS } from "../constants/queryAutocomplete";
import { escapeDoubleQuotes, escapeRegexp } from "../utils/regexp";
enum TypeData { enum TypeData {
metric, metric = "metric",
label, label = "label",
value labelValue = "labelValue"
} }
type FetchDataArgs = { type FetchDataArgs = {
value: string;
urlSuffix: string; urlSuffix: string;
setter: StateUpdater<AutocompleteOptions[]>; setter: StateUpdater<AutocompleteOptions[]>;
type: TypeData; type: TypeData;
params?: URLSearchParams; params?: URLSearchParams;
} }
type FetchQueryArguments = {
valueByContext: string;
metric: string;
label: string;
context: QueryContextType
}
const icons = { const icons = {
[TypeData.metric]: <MetricIcon />, [TypeData.metric]: <MetricIcon/>,
[TypeData.label]: <LabelIcon />, [TypeData.label]: <LabelIcon/>,
[TypeData.value]: <ValueIcon />, [TypeData.labelValue]: <ValueIcon/>,
}; };
const QUERY_LIMIT = 1000; export const useFetchQueryOptions = ({ valueByContext, metric, label, context }: FetchQueryArguments) => {
export const useFetchQueryOptions = ({ metric, label }: { metric: string; label: string }) => {
const { serverUrl } = useAppState(); const { serverUrl } = useAppState();
const { period: { start, end } } = useTimeState(); const { period: { start, end } } = useTimeState();
const { autocompleteCache } = useQueryState();
const queryDispatch = useQueryDispatch();
const [loading, setLoading] = useState(false);
const [value, setValue] = useState(valueByContext);
const debouncedSetValue = debounce(setValue, 800);
useEffect(() => {
debouncedSetValue(valueByContext);
return debouncedSetValue.cancel;
}, [valueByContext, debouncedSetValue]);
const [metrics, setMetrics] = useState<AutocompleteOptions[]>([]); const [metrics, setMetrics] = useState<AutocompleteOptions[]>([]);
const [labels, setLabels] = useState<AutocompleteOptions[]>([]); const [labels, setLabels] = useState<AutocompleteOptions[]>([]);
const [values, setValues] = useState<AutocompleteOptions[]>([]); const [labelValues, setLabelValues] = useState<AutocompleteOptions[]>([]);
const prevParams = useRef<Record<string, URLSearchParams>>({}); const abortControllerRef = useRef(new AbortController());
const getQueryParams = useCallback((params?: Record<string, string>) => { const getQueryParams = useCallback((params?: Record<string, string>) => {
const roundedStart = dayjs(start).startOf("day").valueOf();
const roundedEnd = dayjs(end).endOf("day").valueOf();
return new URLSearchParams({ return new URLSearchParams({
...(params || {}), ...(params || {}),
limit: `${QUERY_LIMIT}`, limit: `${AUTOCOMPLETE_LIMITS.queryLimit}`,
start: `${roundedStart}`, start: `${start}`,
end: `${roundedEnd}` end: `${end}`
}); });
}, [start, end]); }, [start, end]);
const isParamsEqual = (prev: URLSearchParams, next: URLSearchParams) => { const processData = (data: string[], type: TypeData) => {
const queryNext = qs.parse(next.toString()); return data.map(l => ({
const queryPrev = qs.parse(prev.toString()); value: l,
return JSON.stringify(queryPrev) === JSON.stringify(queryNext); type: `${type}`,
icon: icons[type]
}));
}; };
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => { const fetchData = async ({ value, urlSuffix, setter, type, params }: FetchDataArgs) => {
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
const key = {
type,
value,
start: params?.get("start") || "",
end: params?.get("end") || "",
match: params?.get("match[]") || ""
};
setLoading(true);
try { try {
const response = await fetch(`${serverUrl}/api/v1/${urlSuffix}?${params}`); const cachedData = autocompleteCache.get(key);
if (cachedData) {
setter(processData(cachedData, type));
return;
}
const response = await fetch(`${serverUrl}/api/v1/${urlSuffix}?${params}`, { signal });
if (response.ok) { if (response.ok) {
const { data } = await response.json() as { data: string[] }; const { data } = await response.json() as { data: string[] };
setter(data.map(l => ({ setter(processData(data, type));
value: l, queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: data } });
type: `${type}`,
icon: icons[type]
})));
} }
} catch (e) { } catch (e) {
console.error(e); if (e instanceof Error && e.name !== "AbortError") {
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: [] } });
console.error(e);
}
} finally {
setLoading(false);
} }
}; };
// fetch metrics
useEffect(() => { useEffect(() => {
if (!serverUrl) { const isInvalidContext = context !== QueryContextType.metricsql && context !== QueryContextType.empty;
setMetrics([]); if (!serverUrl || !metric || isInvalidContext) {
return; return;
} }
setMetrics([]);
const params = getQueryParams(); const metricReEscaped = escapeDoubleQuotes(escapeRegexp(metric));
const prev = prevParams.current.metrics || new URLSearchParams({});
if (isParamsEqual(params, prev)) return;
fetchData({ fetchData({
value,
urlSuffix: "label/__name__/values", urlSuffix: "label/__name__/values",
setter: setMetrics, setter: setMetrics,
type: TypeData.metric, type: TypeData.metric,
params params: getQueryParams({ "match[]": `{__name__=~".*${metricReEscaped}.*"}` })
}); });
prevParams.current = { ...prevParams.current, metrics: params }; return () => abortControllerRef.current?.abort();
}, [serverUrl, getQueryParams]); }, [serverUrl, value, context, metric]);
// fetch labels
useEffect(() => { useEffect(() => {
const notFoundMetric = !metrics.find(m => m.value === metric); if (!serverUrl || !metric || context !== QueryContextType.label) {
if (!serverUrl || notFoundMetric) {
setLabels([]);
return; return;
} }
setLabels([]);
const params = getQueryParams({ "match[]": metric }); const metricEscaped = escapeDoubleQuotes(metric);
const prev = prevParams.current.labels || new URLSearchParams({});
if (isParamsEqual(params, prev)) return;
fetchData({ fetchData({
value,
urlSuffix: "labels", urlSuffix: "labels",
setter: setLabels, setter: setLabels,
type: TypeData.label, type: TypeData.label,
params params: getQueryParams({ "match[]": `{__name__="${metricEscaped}"}` })
}); });
prevParams.current = { ...prevParams.current, labels: params }; return () => abortControllerRef.current?.abort();
}, [serverUrl, metric, getQueryParams]); }, [serverUrl, value, context, metric]);
// fetch labelValues
useEffect(() => { useEffect(() => {
const notFoundMetric = !metrics.find(m => m.value === metric); if (!serverUrl || !metric || !label || context !== QueryContextType.labelValue) {
const notFoundLabel = !labels.find(l => l.value === label);
if (!serverUrl || notFoundMetric || notFoundLabel) {
setValues([]);
return; return;
} }
setLabelValues([]);
const params = getQueryParams({ "match[]": metric }); const metricEscaped = escapeDoubleQuotes(metric);
const prev = prevParams.current.values || new URLSearchParams({}); const valueReEscaped = escapeDoubleQuotes(escapeRegexp(value));
if (isParamsEqual(params, prev)) return;
fetchData({ fetchData({
value,
urlSuffix: `label/${label}/values`, urlSuffix: `label/${label}/values`,
setter: setValues, setter: setLabelValues,
type: TypeData.value, type: TypeData.labelValue,
params params: getQueryParams({ "match[]": `{__name__="${metricEscaped}", ${label}=~".*${valueReEscaped}.*"}` })
}); });
prevParams.current = { ...prevParams.current, values: params }; return () => abortControllerRef.current?.abort();
}, [serverUrl, metric, label, getQueryParams]); }, [serverUrl, value, context, metric, label]);
return { return {
metrics, metrics,
labels, labels,
values, labelValues,
loading,
}; };
}; };

View File

@ -1,8 +1,9 @@
import React, { useEffect, useState } from "preact/compat"; import React, { useEffect } from "preact/compat";
import { FunctionIcon } from "../components/Main/Icons"; import { FunctionIcon } from "../components/Main/Icons";
import { AutocompleteOptions } from "../components/Main/Autocomplete/Autocomplete"; import { AutocompleteOptions } from "../components/Main/Autocomplete/Autocomplete";
import { marked } from "marked"; import { marked } from "marked";
import MetricsQL from "../assets/MetricsQL.md"; import MetricsQL from "../assets/MetricsQL.md";
import { useQueryDispatch, useQueryState } from "../state/query/QueryStateContext";
const CATEGORY_TAG = "h3"; const CATEGORY_TAG = "h3";
const FUNCTION_TAG = "h4"; const FUNCTION_TAG = "h4";
@ -48,14 +49,14 @@ const processGroups = (groups: NodeListOf<Element>): AutocompleteOptions[] => {
}; };
const useGetMetricsQL = () => { const useGetMetricsQL = () => {
const [metricsQLFunctions, setMetricsQLFunctions] = useState<AutocompleteOptions[]>([]); const { metricsQLFunctions } = useQueryState();
const queryDispatch = useQueryDispatch();
const processMarkdown = (text: string) => { const processMarkdown = (text: string) => {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = marked(text); div.innerHTML = marked(text);
const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`); const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`);
const result = processGroups(groups); return processGroups(groups);
setMetricsQLFunctions(result);
}; };
useEffect(() => { useEffect(() => {
@ -63,12 +64,14 @@ const useGetMetricsQL = () => {
try { try {
const resp = await fetch(MetricsQL); const resp = await fetch(MetricsQL);
const text = await resp.text(); const text = await resp.text();
processMarkdown(text); const result = processMarkdown(text);
queryDispatch({ type: "SET_METRICSQL_FUNCTIONS", payload: result });
} catch (e) { } catch (e) {
console.error("Error fetching or processing the MetricsQL.md file:", e); console.error("Error fetching or processing the MetricsQL.md file:", e);
} }
}; };
if (metricsQLFunctions.length) return;
fetchMarkdown(); fetchMarkdown();
}, []); }, []);

View File

@ -45,7 +45,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { query, queryHistory, autocomplete } = useQueryState(); const { query, queryHistory, autocomplete, autocompleteQuick } = useQueryState();
const queryDispatch = useQueryDispatch(); const queryDispatch = useQueryDispatch();
const timeDispatch = useTimeDispatch(); const timeDispatch = useTimeDispatch();
@ -187,7 +187,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
> >
<QueryEditor <QueryEditor
value={stateQuery[i]} value={stateQuery[i]}
autocomplete={autocomplete} autocomplete={autocomplete || autocompleteQuick}
error={queryErrors[i]} error={queryErrors[i]}
stats={stats[i]} stats={stats[i]}
onArrowUp={createHandlerArrow(-1, i)} onArrowUp={createHandlerArrow(-1, i)}

View File

@ -1,6 +1,11 @@
import { getFromStorage, saveToStorage } from "../../utils/storage"; import { getFromStorage, saveToStorage } from "../../utils/storage";
import { getQueryArray } from "../../utils/query-string"; import { getQueryArray } from "../../utils/query-string";
import { setQueriesToStorage } from "../../pages/CustomPanel/QueryHistory/utils"; import { setQueriesToStorage } from "../../pages/CustomPanel/QueryHistory/utils";
import {
QueryAutocompleteCache,
QueryAutocompleteCacheItem
} from "../../components/Configurators/QueryEditor/QueryAutocompleteCache";
import { AutocompleteOptions } from "../../components/Main/Autocomplete/Autocomplete";
export interface QueryHistoryType { export interface QueryHistoryType {
index: number; index: number;
@ -11,20 +16,28 @@ export interface QueryState {
query: string[]; query: string[];
queryHistory: QueryHistoryType[]; queryHistory: QueryHistoryType[];
autocomplete: boolean; autocomplete: boolean;
autocompleteQuick: boolean;
autocompleteCache: QueryAutocompleteCache;
metricsQLFunctions: AutocompleteOptions[];
} }
export type QueryAction = export type QueryAction =
| { type: "SET_QUERY", payload: string[] } | { type: "SET_QUERY", payload: string[] }
| { type: "SET_QUERY_HISTORY_BY_INDEX", payload: {value: QueryHistoryType, queryNumber: number} } | { type: "SET_QUERY_HISTORY_BY_INDEX", payload: { value: QueryHistoryType, queryNumber: number } }
| { type: "SET_QUERY_HISTORY", payload: QueryHistoryType[] } | { type: "SET_QUERY_HISTORY", payload: QueryHistoryType[] }
| { type: "TOGGLE_AUTOCOMPLETE"} | { type: "TOGGLE_AUTOCOMPLETE" }
| { type: "SET_AUTOCOMPLETE_QUICK", payload: boolean }
| { type: "SET_AUTOCOMPLETE_CACHE", payload: { key: QueryAutocompleteCacheItem, value: string[] } }
| { type: "SET_METRICSQL_FUNCTIONS", payload: AutocompleteOptions[] }
const query = getQueryArray(); const query = getQueryArray();
export const initialQueryState: QueryState = { export const initialQueryState: QueryState = {
query, query,
queryHistory: query.map(q => ({ index: 0, values: [q] })), queryHistory: query.map(q => ({ index: 0, values: [q] })),
autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false, autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false,
autocompleteQuick: false,
autocompleteCache: new QueryAutocompleteCache(),
metricsQLFunctions: [],
}; };
export function reducer(state: QueryState, action: QueryAction): QueryState { export function reducer(state: QueryState, action: QueryAction): QueryState {
@ -52,6 +65,22 @@ export function reducer(state: QueryState, action: QueryAction): QueryState {
...state, ...state,
autocomplete: !state.autocomplete autocomplete: !state.autocomplete
}; };
case "SET_AUTOCOMPLETE_QUICK":
return {
...state,
autocompleteQuick: action.payload
};
case "SET_AUTOCOMPLETE_CACHE": {
state.autocompleteCache.put(action.payload.key, action.payload.value);
return {
...state
};
}
case "SET_METRICSQL_FUNCTIONS":
return {
...state,
metricsQLFunctions: action.payload
};
default: default:
throw new Error(); throw new Error();
} }

View File

@ -153,3 +153,10 @@ export interface ActiveQueriesType {
args?: string; args?: string;
data?: string; data?: string;
} }
export enum QueryContextType {
empty = "empty",
metricsql = "metricsql",
label = "label",
labelValue = "labelValue",
}

View File

@ -1,3 +1,8 @@
export const escapeRegExp = (str: string) => { export const escapeRegexp = (s: string) => {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string // taken from https://stackoverflow.com/a/3561711/274937
return s.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&");
};
export const escapeDoubleQuotes = (s: string) => {
return JSON.stringify(s).slice(1,-1);
}; };

View File

@ -18,7 +18,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"downlevelIteration": true
}, },
"include": [ "include": [
"src" "src"

View File

@ -54,6 +54,7 @@ The sandbox cluster installation is running under the constant load generated by
* `go_gc_pauses_seconds` - the [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram), which shows the duration of GC pauses. * `go_gc_pauses_seconds` - the [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram), which shows the duration of GC pauses.
* `go_scavenge_cpu_seconds_total` - the [counter](https://docs.victoriametrics.com/keyConcepts.html#counter), which shows the total CPU time spent by Go runtime for returning memory to the Operating System. * `go_scavenge_cpu_seconds_total` - the [counter](https://docs.victoriametrics.com/keyConcepts.html#counter), which shows the total CPU time spent by Go runtime for returning memory to the Operating System.
* `go_memlimit_bytes` - the value of [GOMEMLIMIT](https://pkg.go.dev/runtime#hdr-Environment_Variables) environment variable. * `go_memlimit_bytes` - the value of [GOMEMLIMIT](https://pkg.go.dev/runtime#hdr-Environment_Variables) environment variable.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): enhance autocomplete functionality with caching. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5348).
* FEATURE: add field `version` to the response for `/api/v1/status/buildinfo` API for using more efficient API in Grafana for receiving label values. Add additional info about setup Grafana datasource. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5370) and [these docs](https://docs.victoriametrics.com/#grafana-setup) for details. * FEATURE: add field `version` to the response for `/api/v1/status/buildinfo` API for using more efficient API in Grafana for receiving label values. Add additional info about setup Grafana datasource. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5370) and [these docs](https://docs.victoriametrics.com/#grafana-setup) for details.
* FEATURE: add `-search.maxResponseSeries` command-line flag for limiting the number of time series a single query to [`/api/v1/query`](https://docs.victoriametrics.com/keyConcepts.html#instant-query) or [`/api/v1/query_range`](https://docs.victoriametrics.com/keyConcepts.html#range-query) can return. This limit can protect Grafana from high memory usage when the query returns too many series. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5372). * FEATURE: add `-search.maxResponseSeries` command-line flag for limiting the number of time series a single query to [`/api/v1/query`](https://docs.victoriametrics.com/keyConcepts.html#instant-query) or [`/api/v1/query_range`](https://docs.victoriametrics.com/keyConcepts.html#range-query) can return. This limit can protect Grafana from high memory usage when the query returns too many series. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5372).
* FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): ease aggregation for certain alerting rules to keep more useful labels for the context. Before, all extra labels except `job` and `instance` were ignored. See this [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5429) and this [follow-up commit](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/8fb68152e67712ed2c16dcfccf7cf4d0af140835). Thanks to @7840vz. * FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): ease aggregation for certain alerting rules to keep more useful labels for the context. Before, all extra labels except `job` and `instance` were ignored. See this [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5429) and this [follow-up commit](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/8fb68152e67712ed2c16dcfccf7cf4d0af140835). Thanks to @7840vz.

View File

@ -399,6 +399,9 @@ The UI allows exploring query results via graphs and tables. It also provides th
- [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work; - [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work;
- [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs. - [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs.
VMUI provides auto-completion for [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html) functions, metric names, label names and label values. The auto-completion can be enabled
by checking the `Autocomplete` toggle. When the auto-completion is disabled, it can still be triggered for the current cursor position by pressing `ctrl+space`.
VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets
(both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram) (both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram)
and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported). and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported).
@ -2751,7 +2754,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-promscrape.cluster.memberNum string -promscrape.cluster.memberNum string
The number of vmagent instance in the cluster of scrapers. It must be a unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name. See also -promscrape.cluster.memberLabel . See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default "0") The number of vmagent instance in the cluster of scrapers. It must be a unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name. See also -promscrape.cluster.memberLabel . See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default "0")
-promscrape.cluster.memberURLTemplate string -promscrape.cluster.memberURLTemplate string
An optional template for URL to access vmagent instance with the given -promscrape.cluster.memberNum value. Every %d occurrence in the template is substituted with -promscrape.cluster.memberNum at urls to vmagent instances responsible for scraping the given target at /service-discovery page. For example -promscrape.cluster.memberURLTemplate='http://vmagent-%d:8429/targets'. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more details An optional template for URL to access vmagent instance with the given -promscrape.cluster.memberNum value. Every %d occurence in the template is substituted with -promscrape.cluster.memberNum at urls to vmagent instances responsible for scraping the given target at /service-discovery page. For example -promscrape.cluster.memberURLTemplate='http://vmagent-%d:8429/targets'. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more details
-promscrape.cluster.membersCount int -promscrape.cluster.membersCount int
The number of members in a cluster of scrapers. Each member must have a unique -promscrape.cluster.memberNum in the range 0 ... promscrape.cluster.membersCount-1 . Each member then scrapes roughly 1/N of all the targets. By default, cluster scraping is disabled, i.e. a single scraper scrapes all the targets. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default 1) The number of members in a cluster of scrapers. Each member must have a unique -promscrape.cluster.memberNum in the range 0 ... promscrape.cluster.membersCount-1 . Each member then scrapes roughly 1/N of all the targets. By default, cluster scraping is disabled, i.e. a single scraper scrapes all the targets. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default 1)
-promscrape.cluster.name string -promscrape.cluster.name string

View File

@ -407,6 +407,9 @@ The UI allows exploring query results via graphs and tables. It also provides th
- [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work; - [WITH expressions playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/expand-with-exprs) - test how WITH expressions work;
- [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs. - [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - playground for [relabeling](#relabeling) configs.
VMUI provides auto-completion for [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html) functions, metric names, label names and label values. The auto-completion can be enabled
by checking the `Autocomplete` toggle. When the auto-completion is disabled, it can still be triggered for the current cursor position by pressing `ctrl+space`.
VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets VMUI automatically switches from graph view to heatmap view when the query returns [histogram](https://docs.victoriametrics.com/keyConcepts.html#histogram) buckets
(both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram) (both [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram)
and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported). and [VictoriaMetrics histograms](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) are supported).
@ -2759,7 +2762,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-promscrape.cluster.memberNum string -promscrape.cluster.memberNum string
The number of vmagent instance in the cluster of scrapers. It must be a unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name. See also -promscrape.cluster.memberLabel . See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default "0") The number of vmagent instance in the cluster of scrapers. It must be a unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name. See also -promscrape.cluster.memberLabel . See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default "0")
-promscrape.cluster.memberURLTemplate string -promscrape.cluster.memberURLTemplate string
An optional template for URL to access vmagent instance with the given -promscrape.cluster.memberNum value. Every %d occurrence in the template is substituted with -promscrape.cluster.memberNum at urls to vmagent instances responsible for scraping the given target at /service-discovery page. For example -promscrape.cluster.memberURLTemplate='http://vmagent-%d:8429/targets'. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more details An optional template for URL to access vmagent instance with the given -promscrape.cluster.memberNum value. Every %d occurence in the template is substituted with -promscrape.cluster.memberNum at urls to vmagent instances responsible for scraping the given target at /service-discovery page. For example -promscrape.cluster.memberURLTemplate='http://vmagent-%d:8429/targets'. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more details
-promscrape.cluster.membersCount int -promscrape.cluster.membersCount int
The number of members in a cluster of scrapers. Each member must have a unique -promscrape.cluster.memberNum in the range 0 ... promscrape.cluster.membersCount-1 . Each member then scrapes roughly 1/N of all the targets. By default, cluster scraping is disabled, i.e. a single scraper scrapes all the targets. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default 1) The number of members in a cluster of scrapers. Each member must have a unique -promscrape.cluster.memberNum in the range 0 ... promscrape.cluster.membersCount-1 . Each member then scrapes roughly 1/N of all the targets. By default, cluster scraping is disabled, i.e. a single scraper scrapes all the targets. See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets for more info (default 1)
-promscrape.cluster.name string -promscrape.cluster.name string