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;
|
||||
- [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
|
||||
(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).
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.349e6522.css",
|
||||
"main.js": "./static/js/main.c93073e5.js",
|
||||
"main.css": "./static/css/main.fb353c1e.css",
|
||||
"main.js": "./static/js/main.5bcddddc.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"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.349e6522.css",
|
||||
"static/js/main.c93073e5.js"
|
||||
"static/css/main.fb353c1e.css",
|
||||
"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.
|
||||
|
||||
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
|
||||
|
||||
@ -1049,6 +1049,15 @@ Metric names are stripped from the resulting series. Add [keep_metric_names](#ke
|
||||
|
||||
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(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.
|
||||
|
||||
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
|
||||
|
||||
@ -1049,6 +1049,15 @@ Metric names are stripped from the resulting series. Add [keep_metric_names](#ke
|
||||
|
||||
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(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 useEventListener from "../../../hooks/useEventListener";
|
||||
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 { autocomplete } = useQueryState();
|
||||
@ -32,11 +32,16 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
|
||||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
||||
};
|
||||
|
||||
const onChangeQuickAutocomplete = () => {
|
||||
queryDispatch({ type: "SET_AUTOCOMPLETE_QUICK", payload: true });
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const { code, altKey } = e;
|
||||
if (code === "KeyA" && altKey) {
|
||||
/** @see AUTOCOMPLETE_QUICK_KEY */
|
||||
const { code, ctrlKey, altKey } = e;
|
||||
if (code === "Space" && (ctrlKey || altKey)) {
|
||||
e.preventDefault();
|
||||
onChangeAutocomplete();
|
||||
onChangeQuickAutocomplete();
|
||||
}
|
||||
};
|
||||
|
||||
@ -49,7 +54,7 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
|
||||
"vm-additional-settings_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<Tooltip title={AUTOCOMPLETE_KEY}>
|
||||
<Tooltip title={<>Quick tip: {AUTOCOMPLETE_QUICK_KEY}</>}>
|
||||
<Switch
|
||||
label={"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 { KeyboardEvent } from "react";
|
||||
import { KeyboardEvent, useEffect } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import QueryEditorAutocomplete from "./QueryEditorAutocomplete";
|
||||
@ -7,6 +7,7 @@ import "./style.scss";
|
||||
import { QueryStats } from "../../../api/types";
|
||||
import { partialWarning, seriesFetchedWarning } from "./warningText";
|
||||
import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
||||
import { useQueryDispatch } from "../../../state/query/QueryStateContext";
|
||||
|
||||
export interface QueryEditorProps {
|
||||
onChange: (query: string) => void;
|
||||
@ -38,6 +39,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
||||
const [caretPosition, setCaretPosition] = useState([0, 0]);
|
||||
const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
|
||||
const queryDispatch = useQueryDispatch();
|
||||
|
||||
const warning = [
|
||||
{
|
||||
@ -100,6 +102,10 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
setCaretPosition(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryDispatch({ type: "SET_AUTOCOMPLETE_QUICK", payload: false });
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
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 { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
||||
import { getTextWidth } from "../../../utils/uplot";
|
||||
import { escapeRegExp } from "../../../utils/regexp";
|
||||
import { escapeRegexp } from "../../../utils/regexp";
|
||||
import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
|
||||
|
||||
enum ContextType {
|
||||
empty = "empty",
|
||||
metricsql = "metricsql",
|
||||
label = "label",
|
||||
value = "value",
|
||||
}
|
||||
import { RefreshIcon } from "../../Main/Icons";
|
||||
import { QueryContextType } from "../../../types";
|
||||
import { AUTOCOMPLETE_LIMITS, AUTOCOMPLETE_MIN_SYMBOLS } from "../../../constants/queryAutocomplete";
|
||||
|
||||
interface QueryEditorAutocompleteProps {
|
||||
value: string;
|
||||
@ -37,65 +33,70 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
}, [value]);
|
||||
|
||||
const label = useMemo(() => {
|
||||
const regexp = /[a-z_]\w*(?=\s*(=|!=|=~|!~))/g;
|
||||
const regexp = /[a-z_:-][\w\-.:/]*\b(?=\s*(=|!=|=~|!~))/g;
|
||||
const match = value.match(regexp);
|
||||
return match ? match[match.length - 1] : "";
|
||||
}, [value]);
|
||||
|
||||
|
||||
const metricRegexp = new RegExp(`\\(?(${escapeRegExp(metric)})$`, "g");
|
||||
const labelRegexp = /[{.,].?(\w+)$/gm;
|
||||
const valueRegexp = new RegExp(`(${escapeRegExp(metric)})?{?.+${escapeRegExp(label)}="?([^"]*)$`, "g");
|
||||
|
||||
const context = useMemo(() => {
|
||||
[metricRegexp, labelRegexp, valueRegexp].forEach(regexp => regexp.lastIndex = 0);
|
||||
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]);
|
||||
if (!value) return QueryContextType.empty;
|
||||
|
||||
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(() => {
|
||||
switch (context) {
|
||||
case ContextType.metricsql:
|
||||
case QueryContextType.metricsql:
|
||||
return [...metrics, ...metricsqlFunctions];
|
||||
case ContextType.label:
|
||||
case QueryContextType.label:
|
||||
return labels;
|
||||
case ContextType.value:
|
||||
return values;
|
||||
case QueryContextType.labelValue:
|
||||
return labelValues;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [context, metrics, labels, values]);
|
||||
|
||||
const valueByContext = useMemo(() => {
|
||||
if (value.length !== caretPosition[1]) return value;
|
||||
|
||||
const wordMatch = value.match(/([\w_]+)$/) || [];
|
||||
return wordMatch[1] || "";
|
||||
}, [context, caretPosition, value]);
|
||||
}, [context, metrics, labels, labelValues]);
|
||||
|
||||
const handleSelect = (insert: string) => {
|
||||
const wordMatch = value.match(/([\w_]+)$/);
|
||||
const wordMatchIndex = wordMatch?.index !== undefined ? wordMatch.index : value.length;
|
||||
const beforeInsert = value.substring(0, wordMatchIndex);
|
||||
const afterInsert = value.substring(wordMatchIndex + (wordMatch?.[1].length || 0));
|
||||
// Find the start and end of valueByContext in the query string
|
||||
const startIndexOfValueByContext = value.lastIndexOf(valueByContext, caretPosition[0]);
|
||||
const endIndexOfValueByContext = startIndexOfValueByContext + valueByContext.length;
|
||||
|
||||
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 needsQuote = beforeInsert[beforeInsert.length - 1] !== quote;
|
||||
const needsQuote = !beforeValueByContext.endsWith(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);
|
||||
};
|
||||
|
||||
@ -113,16 +114,23 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
}, [anchorEl, caretPosition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
disabledFullScreen
|
||||
value={valueByContext}
|
||||
options={options}
|
||||
options={options?.length < AUTOCOMPLETE_LIMITS.queryLimit ? options : []}
|
||||
anchor={anchorEl}
|
||||
minLength={context === ContextType.metricsql ? 2 : 0}
|
||||
minLength={AUTOCOMPLETE_MIN_SYMBOLS[context]}
|
||||
offset={{ top: 0, left: leftOffset }}
|
||||
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;
|
||||
|
||||
&-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
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
|
||||
disabledFullScreen?: boolean
|
||||
offset?: {top: number, left: number}
|
||||
maxDisplayResults?: {limit: number, message?: string}
|
||||
onSelect: (val: string) => void
|
||||
onOpenAutocomplete?: (val: boolean) => void
|
||||
onFoundOptions?: (val: AutocompleteOptions[]) => void
|
||||
@ -48,6 +49,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
label,
|
||||
disabledFullScreen,
|
||||
offset,
|
||||
maxDisplayResults,
|
||||
onSelect,
|
||||
onOpenAutocomplete,
|
||||
onFoundOptions
|
||||
@ -56,6 +58,8 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
const wrapperEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [focusOption, setFocusOption] = useState<{index: number, type?: FocusType}>({ index: -1 });
|
||||
const [showMessage, setShowMessage] = useState("");
|
||||
const [totalFound, setTotalFound] = useState(0);
|
||||
|
||||
const {
|
||||
value: openAutocomplete,
|
||||
@ -68,7 +72,14 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
try {
|
||||
const regexp = new RegExp(String(value.trim()), "i");
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
@ -133,7 +144,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setOpenAutocomplete(value.length >= minLength);
|
||||
}, [value]);
|
||||
}, [value, options]);
|
||||
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
|
||||
@ -170,7 +181,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
ref={wrapperEl}
|
||||
>
|
||||
{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
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
@ -192,6 +203,11 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showMessage && (
|
||||
<div className="vm-autocomplete-message">
|
||||
Shown {maxDisplayResults?.limit} results out of {totalFound}. {showMessage}
|
||||
</div>
|
||||
)}
|
||||
{foundOptions[focusOption.index]?.description && (
|
||||
<div className="vm-autocomplete-info">
|
||||
<div className="vm-autocomplete-info__type">
|
||||
|
@ -16,16 +16,33 @@
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&-info {
|
||||
position: absolute;
|
||||
top: calc(100% + 1px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 450px;
|
||||
&-info,
|
||||
&-message {
|
||||
padding: $padding-global;
|
||||
background-color: $color-background-block;
|
||||
box-shadow: $box-shadow-popper;
|
||||
border-radius: $border-radius-small;
|
||||
border-top: $border-divider;
|
||||
}
|
||||
|
||||
&-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;
|
||||
|
||||
&__type {
|
||||
|
@ -4,9 +4,8 @@ import { VisibilityIcon } from "../../Icons";
|
||||
import GraphTips from "../../../Chart/GraphTips/GraphTips";
|
||||
|
||||
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 = [
|
||||
{
|
||||
@ -33,8 +32,8 @@ const keyList = [
|
||||
description: "Toggle multiple queries"
|
||||
},
|
||||
{
|
||||
keys: AUTOCOMPLETE_KEY,
|
||||
description: "Toggle autocomplete"
|
||||
keys: AUTOCOMPLETE_QUICK_KEY,
|
||||
description: "Show quick autocomplete tips"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -83,7 +83,6 @@ const TextField: FC<TextFieldProps> = ({
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
onKeyDown && onKeyDown(e);
|
||||
updateCaretPosition(e.currentTarget);
|
||||
const { key, ctrlKey, metaKey } = e;
|
||||
const isEnter = key === "Enter";
|
||||
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>) => {
|
||||
if (disabled) return;
|
||||
onChange && onChange(e.currentTarget.value);
|
||||
@ -135,6 +138,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
autoCapitalize={"none"}
|
||||
onInput={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onMouseUp={handleMouseUp}
|
||||
@ -152,6 +156,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
autoCapitalize={"none"}
|
||||
onInput={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
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 { useTimeState } from "../state/time/TimeStateContext";
|
||||
import { useCallback } from "react";
|
||||
import qs from "qs";
|
||||
import dayjs from "dayjs";
|
||||
import debounce from "lodash.debounce";
|
||||
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 {
|
||||
metric,
|
||||
label,
|
||||
value
|
||||
metric = "metric",
|
||||
label = "label",
|
||||
labelValue = "labelValue"
|
||||
}
|
||||
|
||||
type FetchDataArgs = {
|
||||
value: string;
|
||||
urlSuffix: string;
|
||||
setter: StateUpdater<AutocompleteOptions[]>;
|
||||
type: TypeData;
|
||||
params?: URLSearchParams;
|
||||
}
|
||||
|
||||
type FetchQueryArguments = {
|
||||
valueByContext: string;
|
||||
metric: string;
|
||||
label: string;
|
||||
context: QueryContextType
|
||||
}
|
||||
|
||||
const icons = {
|
||||
[TypeData.metric]: <MetricIcon/>,
|
||||
[TypeData.label]: <LabelIcon/>,
|
||||
[TypeData.value]: <ValueIcon />,
|
||||
[TypeData.labelValue]: <ValueIcon/>,
|
||||
};
|
||||
|
||||
const QUERY_LIMIT = 1000;
|
||||
|
||||
export const useFetchQueryOptions = ({ metric, label }: { metric: string; label: string }) => {
|
||||
export const useFetchQueryOptions = ({ valueByContext, metric, label, context }: FetchQueryArguments) => {
|
||||
const { serverUrl } = useAppState();
|
||||
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 [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 roundedStart = dayjs(start).startOf("day").valueOf();
|
||||
const roundedEnd = dayjs(end).endOf("day").valueOf();
|
||||
|
||||
return new URLSearchParams({
|
||||
...(params || {}),
|
||||
limit: `${QUERY_LIMIT}`,
|
||||
start: `${roundedStart}`,
|
||||
end: `${roundedEnd}`
|
||||
limit: `${AUTOCOMPLETE_LIMITS.queryLimit}`,
|
||||
start: `${start}`,
|
||||
end: `${end}`
|
||||
});
|
||||
}, [start, end]);
|
||||
|
||||
const isParamsEqual = (prev: URLSearchParams, next: URLSearchParams) => {
|
||||
const queryNext = qs.parse(next.toString());
|
||||
const queryPrev = qs.parse(prev.toString());
|
||||
return JSON.stringify(queryPrev) === JSON.stringify(queryNext);
|
||||
};
|
||||
|
||||
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => {
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/api/v1/${urlSuffix}?${params}`);
|
||||
if (response.ok) {
|
||||
const { data } = await response.json() as { data: string[] };
|
||||
setter(data.map(l => ({
|
||||
const processData = (data: string[], type: TypeData) => {
|
||||
return data.map(l => ({
|
||||
value: l,
|
||||
type: `${type}`,
|
||||
icon: icons[type]
|
||||
})));
|
||||
}));
|
||||
};
|
||||
|
||||
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 {
|
||||
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) {
|
||||
const { data } = await response.json() as { data: string[] };
|
||||
setter(processData(data, type));
|
||||
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: data } });
|
||||
}
|
||||
} catch (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(() => {
|
||||
if (!serverUrl) {
|
||||
setMetrics([]);
|
||||
const isInvalidContext = context !== QueryContextType.metricsql && context !== QueryContextType.empty;
|
||||
if (!serverUrl || !metric || isInvalidContext) {
|
||||
return;
|
||||
}
|
||||
setMetrics([]);
|
||||
|
||||
const params = getQueryParams();
|
||||
const prev = prevParams.current.metrics || new URLSearchParams({});
|
||||
if (isParamsEqual(params, prev)) return;
|
||||
const metricReEscaped = escapeDoubleQuotes(escapeRegexp(metric));
|
||||
|
||||
fetchData({
|
||||
value,
|
||||
urlSuffix: "label/__name__/values",
|
||||
setter: setMetrics,
|
||||
type: TypeData.metric,
|
||||
params
|
||||
params: getQueryParams({ "match[]": `{__name__=~".*${metricReEscaped}.*"}` })
|
||||
});
|
||||
|
||||
prevParams.current = { ...prevParams.current, metrics: params };
|
||||
}, [serverUrl, getQueryParams]);
|
||||
return () => abortControllerRef.current?.abort();
|
||||
}, [serverUrl, value, context, metric]);
|
||||
|
||||
// fetch labels
|
||||
useEffect(() => {
|
||||
const notFoundMetric = !metrics.find(m => m.value === metric);
|
||||
if (!serverUrl || notFoundMetric) {
|
||||
setLabels([]);
|
||||
if (!serverUrl || !metric || context !== QueryContextType.label) {
|
||||
return;
|
||||
}
|
||||
setLabels([]);
|
||||
|
||||
const params = getQueryParams({ "match[]": metric });
|
||||
const prev = prevParams.current.labels || new URLSearchParams({});
|
||||
if (isParamsEqual(params, prev)) return;
|
||||
const metricEscaped = escapeDoubleQuotes(metric);
|
||||
|
||||
fetchData({
|
||||
value,
|
||||
urlSuffix: "labels",
|
||||
setter: setLabels,
|
||||
type: TypeData.label,
|
||||
params
|
||||
params: getQueryParams({ "match[]": `{__name__="${metricEscaped}"}` })
|
||||
});
|
||||
|
||||
prevParams.current = { ...prevParams.current, labels: params };
|
||||
}, [serverUrl, metric, getQueryParams]);
|
||||
return () => abortControllerRef.current?.abort();
|
||||
}, [serverUrl, value, context, metric]);
|
||||
|
||||
// fetch labelValues
|
||||
useEffect(() => {
|
||||
const notFoundMetric = !metrics.find(m => m.value === metric);
|
||||
const notFoundLabel = !labels.find(l => l.value === label);
|
||||
if (!serverUrl || notFoundMetric || notFoundLabel) {
|
||||
setValues([]);
|
||||
if (!serverUrl || !metric || !label || context !== QueryContextType.labelValue) {
|
||||
return;
|
||||
}
|
||||
setLabelValues([]);
|
||||
|
||||
const params = getQueryParams({ "match[]": metric });
|
||||
const prev = prevParams.current.values || new URLSearchParams({});
|
||||
if (isParamsEqual(params, prev)) return;
|
||||
const metricEscaped = escapeDoubleQuotes(metric);
|
||||
const valueReEscaped = escapeDoubleQuotes(escapeRegexp(value));
|
||||
|
||||
fetchData({
|
||||
value,
|
||||
urlSuffix: `label/${label}/values`,
|
||||
setter: setValues,
|
||||
type: TypeData.value,
|
||||
params
|
||||
setter: setLabelValues,
|
||||
type: TypeData.labelValue,
|
||||
params: getQueryParams({ "match[]": `{__name__="${metricEscaped}", ${label}=~".*${valueReEscaped}.*"}` })
|
||||
});
|
||||
|
||||
prevParams.current = { ...prevParams.current, values: params };
|
||||
}, [serverUrl, metric, label, getQueryParams]);
|
||||
return () => abortControllerRef.current?.abort();
|
||||
}, [serverUrl, value, context, metric, label]);
|
||||
|
||||
return {
|
||||
metrics,
|
||||
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 { AutocompleteOptions } from "../components/Main/Autocomplete/Autocomplete";
|
||||
import { marked } from "marked";
|
||||
import MetricsQL from "../assets/MetricsQL.md";
|
||||
import { useQueryDispatch, useQueryState } from "../state/query/QueryStateContext";
|
||||
|
||||
const CATEGORY_TAG = "h3";
|
||||
const FUNCTION_TAG = "h4";
|
||||
@ -48,14 +49,14 @@ const processGroups = (groups: NodeListOf<Element>): AutocompleteOptions[] => {
|
||||
};
|
||||
|
||||
const useGetMetricsQL = () => {
|
||||
const [metricsQLFunctions, setMetricsQLFunctions] = useState<AutocompleteOptions[]>([]);
|
||||
const { metricsQLFunctions } = useQueryState();
|
||||
const queryDispatch = useQueryDispatch();
|
||||
|
||||
const processMarkdown = (text: string) => {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = marked(text);
|
||||
const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`);
|
||||
const result = processGroups(groups);
|
||||
setMetricsQLFunctions(result);
|
||||
return processGroups(groups);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -63,12 +64,14 @@ const useGetMetricsQL = () => {
|
||||
try {
|
||||
const resp = await fetch(MetricsQL);
|
||||
const text = await resp.text();
|
||||
processMarkdown(text);
|
||||
const result = processMarkdown(text);
|
||||
queryDispatch({ type: "SET_METRICSQL_FUNCTIONS", payload: result });
|
||||
} catch (e) {
|
||||
console.error("Error fetching or processing the MetricsQL.md file:", e);
|
||||
}
|
||||
};
|
||||
|
||||
if (metricsQLFunctions.length) return;
|
||||
fetchMarkdown();
|
||||
}, []);
|
||||
|
||||
|
@ -45,7 +45,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { query, queryHistory, autocomplete } = useQueryState();
|
||||
const { query, queryHistory, autocomplete, autocompleteQuick } = useQueryState();
|
||||
const queryDispatch = useQueryDispatch();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
@ -187,7 +187,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||
>
|
||||
<QueryEditor
|
||||
value={stateQuery[i]}
|
||||
autocomplete={autocomplete}
|
||||
autocomplete={autocomplete || autocompleteQuick}
|
||||
error={queryErrors[i]}
|
||||
stats={stats[i]}
|
||||
onArrowUp={createHandlerArrow(-1, i)}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
import { getQueryArray } from "../../utils/query-string";
|
||||
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 {
|
||||
index: number;
|
||||
@ -11,7 +16,9 @@ export interface QueryState {
|
||||
query: string[];
|
||||
queryHistory: QueryHistoryType[];
|
||||
autocomplete: boolean;
|
||||
|
||||
autocompleteQuick: boolean;
|
||||
autocompleteCache: QueryAutocompleteCache;
|
||||
metricsQLFunctions: AutocompleteOptions[];
|
||||
}
|
||||
|
||||
export type QueryAction =
|
||||
@ -19,12 +26,18 @@ export type QueryAction =
|
||||
| { type: "SET_QUERY_HISTORY_BY_INDEX", payload: { value: QueryHistoryType, queryNumber: number } }
|
||||
| { type: "SET_QUERY_HISTORY", payload: QueryHistoryType[] }
|
||||
| { 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();
|
||||
export const initialQueryState: QueryState = {
|
||||
query,
|
||||
queryHistory: query.map(q => ({ index: 0, values: [q] })),
|
||||
autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false,
|
||||
autocompleteQuick: false,
|
||||
autocompleteCache: new QueryAutocompleteCache(),
|
||||
metricsQLFunctions: [],
|
||||
};
|
||||
|
||||
export function reducer(state: QueryState, action: QueryAction): QueryState {
|
||||
@ -52,6 +65,22 @@ export function reducer(state: QueryState, action: QueryAction): QueryState {
|
||||
...state,
|
||||
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:
|
||||
throw new Error();
|
||||
}
|
||||
|
@ -153,3 +153,10 @@ export interface ActiveQueriesType {
|
||||
args?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export enum QueryContextType {
|
||||
empty = "empty",
|
||||
metricsql = "metricsql",
|
||||
label = "label",
|
||||
labelValue = "labelValue",
|
||||
}
|
||||
|
@ -1,3 +1,8 @@
|
||||
export const escapeRegExp = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||
export const escapeRegexp = (s: 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,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"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_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.
|
||||
* 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 `-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.
|
||||
|
@ -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;
|
||||
- [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
|
||||
(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).
|
||||
@ -2751,7 +2754,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-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")
|
||||
-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
|
||||
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
|
||||
|
@ -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;
|
||||
- [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
|
||||
(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).
|
||||
@ -2759,7 +2762,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-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")
|
||||
-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
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user