mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 12:31:07 +01:00
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:
parent
0f91f83639
commit
1a5cdb4790
@ -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).
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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
1
app/vmselect/vmui/static/css/main.fb353c1e.css
Normal file
1
app/vmselect/vmui/static/css/main.fb353c1e.css
Normal file
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.5bcddddc.js
Normal file
2
app/vmselect/vmui/static/js/main.5bcddddc.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -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}
|
||||||
|
14
app/vmui/packages/vmui/src/constants/queryAutocomplete.ts
Normal file
14
app/vmui/packages/vmui/src/constants/queryAutocomplete.ts
Normal 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,
|
||||||
|
};
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -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)}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user