= ({
}, [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
= ({
}, [anchorEl, caretPosition]);
return (
-
+ <>
+
+ {loading &&
}
+ >
);
};
diff --git a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
index 6e9d4277f..46eda2e38 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
+++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
@@ -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;
}
}
diff --git a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
index 827340b50..8c04a3ea5 100644
--- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
@@ -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 = ({
label,
disabledFullScreen,
offset,
+ maxDisplayResults,
onSelect,
onOpenAutocomplete,
onFoundOptions
@@ -56,6 +58,8 @@ const Autocomplete: FC = ({
const wrapperEl = useRef(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 = ({
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 = ({
useEffect(() => {
setOpenAutocomplete(value.length >= minLength);
- }, [value]);
+ }, [value, options]);
useEventListener("keydown", handleKeyDown);
@@ -170,7 +181,7 @@ const Autocomplete: FC = ({
ref={wrapperEl}
>
{displayNoOptionsText && {noOptionsText}
}
- {foundOptions.map((option, i) =>
+ {!(foundOptions.length === 1 && foundOptions[0]?.value === value) && foundOptions.map((option, i) =>
= ({
)}
+ {showMessage && (
+
+ Shown {maxDisplayResults?.limit} results out of {totalFound}. {showMessage}
+
+ )}
{foundOptions[focusOption.index]?.description && (
diff --git a/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss b/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss
index d5d3f360e..10154f5bd 100644
--- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss
@@ -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 {
diff --git a/app/vmui/packages/vmui/src/components/Main/ShortcutKeys/constants/keyList.tsx b/app/vmui/packages/vmui/src/components/Main/ShortcutKeys/constants/keyList.tsx
index 5072beaa8..1db934772 100644
--- a/app/vmui/packages/vmui/src/components/Main/ShortcutKeys/constants/keyList.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/ShortcutKeys/constants/keyList.tsx
@@ -4,9 +4,8 @@ import { VisibilityIcon } from "../../Icons";
import GraphTips from "../../../Chart/GraphTips/GraphTips";
const ctrlMeta =
{isMacOs() ? "Cmd" : "Ctrl"}
;
-const altMeta =
{isMacOs() ? "Option" : "Alt"}
;
-export const AUTOCOMPLETE_KEY = <>{altMeta} +
A
>;
+export const AUTOCOMPLETE_QUICK_KEY = <>{
{isMacOs() ? "Option" : "Ctrl"}
} +
Space
>;
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"
}
]
},
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
index ce1b21caa..f95b4194e 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
@@ -83,7 +83,6 @@ const TextField: FC
= ({
const handleKeyDown = (e: KeyboardEvent) => {
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 = ({
}
};
+ const handleKeyUp = (e: KeyboardEvent) => {
+ updateCaretPosition(e.currentTarget);
+ };
+
const handleChange = (e: FormEvent) => {
if (disabled) return;
onChange && onChange(e.currentTarget.value);
@@ -135,6 +138,7 @@ const TextField: FC = ({
autoCapitalize={"none"}
onInput={handleChange}
onKeyDown={handleKeyDown}
+ onKeyUp={handleKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseUp={handleMouseUp}
@@ -152,6 +156,7 @@ const TextField: FC = ({
autoCapitalize={"none"}
onInput={handleChange}
onKeyDown={handleKeyDown}
+ onKeyUp={handleKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseUp={handleMouseUp}
diff --git a/app/vmui/packages/vmui/src/constants/queryAutocomplete.ts b/app/vmui/packages/vmui/src/constants/queryAutocomplete.ts
new file mode 100644
index 000000000..763991fca
--- /dev/null
+++ b/app/vmui/packages/vmui/src/constants/queryAutocomplete.ts
@@ -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,
+};
diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx b/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
index b60a12e72..98d537c30 100644
--- a/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
+++ b/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
@@ -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;
type: TypeData;
params?: URLSearchParams;
}
+type FetchQueryArguments = {
+ valueByContext: string;
+ metric: string;
+ label: string;
+ context: QueryContextType
+}
+
const icons = {
- [TypeData.metric]: ,
- [TypeData.label]: ,
- [TypeData.value]: ,
+ [TypeData.metric]: ,
+ [TypeData.label]: ,
+ [TypeData.labelValue]: ,
};
-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([]);
const [labels, setLabels] = useState([]);
- const [values, setValues] = useState([]);
+ const [labelValues, setLabelValues] = useState([]);
- const prevParams = useRef>({});
+ const abortControllerRef = useRef(new AbortController());
const getQueryParams = useCallback((params?: Record) => {
- 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 processData = (data: string[], type: TypeData) => {
+ return data.map(l => ({
+ value: l,
+ 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 {
- 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) {
const { data } = await response.json() as { data: string[] };
- setter(data.map(l => ({
- value: l,
- type: `${type}`,
- icon: icons[type]
- })));
+ setter(processData(data, type));
+ queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: data } });
}
} 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(() => {
- 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,
};
};
diff --git a/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx b/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
index fbe3960f4..fb930c4ba 100644
--- a/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
+++ b/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
@@ -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): AutocompleteOptions[] => {
};
const useGetMetricsQL = () => {
- const [metricsQLFunctions, setMetricsQLFunctions] = useState([]);
+ 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();
}, []);
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
index 011bfb12d..0b0133499 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
@@ -45,7 +45,7 @@ const QueryConfigurator: FC = ({
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 = ({
>
({ 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();
}
diff --git a/app/vmui/packages/vmui/src/types/index.ts b/app/vmui/packages/vmui/src/types/index.ts
index 9eacbe6e5..c04aea5bd 100644
--- a/app/vmui/packages/vmui/src/types/index.ts
+++ b/app/vmui/packages/vmui/src/types/index.ts
@@ -153,3 +153,10 @@ export interface ActiveQueriesType {
args?: string;
data?: string;
}
+
+export enum QueryContextType {
+ empty = "empty",
+ metricsql = "metricsql",
+ label = "label",
+ labelValue = "labelValue",
+}
diff --git a/app/vmui/packages/vmui/src/utils/regexp.ts b/app/vmui/packages/vmui/src/utils/regexp.ts
index 96e6f859c..19836626e 100644
--- a/app/vmui/packages/vmui/src/utils/regexp.ts
+++ b/app/vmui/packages/vmui/src/utils/regexp.ts
@@ -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);
};
diff --git a/app/vmui/packages/vmui/tsconfig.json b/app/vmui/packages/vmui/tsconfig.json
index a273b0cfc..9670db896 100644
--- a/app/vmui/packages/vmui/tsconfig.json
+++ b/app/vmui/packages/vmui/tsconfig.json
@@ -18,7 +18,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
- "jsx": "react-jsx"
+ "jsx": "react-jsx",
+ "downlevelIteration": true
},
"include": [
"src"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 04f9089a9..a8cd201b4 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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.
diff --git a/docs/README.md b/docs/README.md
index a8ed5e69c..5916e2343 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index cc792dac1..16e54fe38 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -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