From c5044cdba98dd7d554c3ca437c78f04ffecd41c4 Mon Sep 17 00:00:00 2001
From: Yury Molodov <yurymolodov@gmail.com>
Date: Tue, 10 Oct 2023 10:38:08 +0200
Subject: [PATCH] vmui: enhancement of autocomplete feature (#5051)

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4993
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3006
---
 app/vmui/.gitignore                           |   2 +
 app/vmui/Makefile                             |   5 +-
 app/vmui/packages/vmui/package.json           |   4 +-
 app/vmui/packages/vmui/src/api/query-range.ts |   2 -
 .../Configurators/QueryEditor/QueryEditor.tsx |  66 +++++----
 .../QueryEditor/QueryEditorAutocomplete.tsx   | 129 ++++++++++++++++++
 .../Main/Autocomplete/Autocomplete.tsx        |  91 ++++++++----
 .../components/Main/Autocomplete/style.scss   |  31 +++++
 .../vmui/src/components/Main/Icons/index.tsx  |  51 +++++++
 .../src/components/Main/Select/Select.tsx     |   5 +-
 .../components/Main/TextField/TextField.tsx   |  35 ++++-
 .../vmui/src/hooks/useFetchQueryOptions.ts    |  32 -----
 .../vmui/src/hooks/useFetchQueryOptions.tsx   |  94 +++++++++++++
 .../vmui/src/hooks/useGetMetricsQL.tsx        |  78 +++++++++++
 .../QueryConfigurator/QueryConfigurator.tsx   |   3 -
 .../vmui/src/pages/CustomPanel/index.tsx      |   3 -
 .../ExploreLogsHeader/ExploreLogsHeader.tsx   |   1 -
 .../vmui/src/styles/components/list.scss      |   8 ++
 .../packages/vmui/src/types/markdown.d.ts     |   4 +
 app/vmui/packages/vmui/src/utils/regexp.ts    |   3 +
 docs/CHANGELOG.md                             |   1 +
 21 files changed, 541 insertions(+), 107 deletions(-)
 create mode 100644 app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditorAutocomplete.tsx
 delete mode 100644 app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.ts
 create mode 100644 app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
 create mode 100644 app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
 create mode 100644 app/vmui/packages/vmui/src/types/markdown.d.ts
 create mode 100644 app/vmui/packages/vmui/src/utils/regexp.ts

diff --git a/app/vmui/.gitignore b/app/vmui/.gitignore
index 8f007b3120..5fa6b0d6ec 100644
--- a/app/vmui/.gitignore
+++ b/app/vmui/.gitignore
@@ -105,3 +105,5 @@ dist
 
 # WebStorm etc
 .idea/
+
+MetricsQL.md
diff --git a/app/vmui/Makefile b/app/vmui/Makefile
index 39a65358ff..eb3354044c 100644
--- a/app/vmui/Makefile
+++ b/app/vmui/Makefile
@@ -1,9 +1,12 @@
 # All these commands must run from repository root.
 
+copy-metricsql-docs:
+	cp docs/MetricsQL.md app/vmui/packages/vmui/src/assets/MetricsQL.md
+
 vmui-package-base-image:
 	docker build -t vmui-builder-image -f app/vmui/Dockerfile-build ./app/vmui
 
-vmui-build: vmui-package-base-image
+vmui-build: copy-metricsql-docs vmui-package-base-image
 	docker run --rm \
 		--user $(shell id -u):$(shell id -g) \
 		--mount type=bind,src="$(shell pwd)/app/vmui",dst=/build \
diff --git a/app/vmui/packages/vmui/package.json b/app/vmui/packages/vmui/package.json
index ff05078f7d..ad3e2d91ad 100644
--- a/app/vmui/packages/vmui/package.json
+++ b/app/vmui/packages/vmui/package.json
@@ -30,13 +30,15 @@
     "web-vitals": "^3.3.2"
   },
   "scripts": {
+    "prestart": "npm run update-metricsql",
     "start": "react-app-rewired start",
     "start:logs": "cross-env REACT_APP_LOGS=true npm run start",
     "build": "GENERATE_SOURCEMAP=false react-app-rewired build",
     "build:logs": "cross-env REACT_APP_LOGS=true npm run build",
     "lint": "eslint src --ext tsx,ts",
     "lint:fix": "eslint src --ext tsx,ts --fix",
-    "analyze": "source-map-explorer 'build/static/js/*.js'"
+    "analyze": "source-map-explorer 'build/static/js/*.js'",
+    "copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true"
   },
   "eslintConfig": {
     "extends": [
diff --git a/app/vmui/packages/vmui/src/api/query-range.ts b/app/vmui/packages/vmui/src/api/query-range.ts
index 850760fe8c..28a3679d64 100644
--- a/app/vmui/packages/vmui/src/api/query-range.ts
+++ b/app/vmui/packages/vmui/src/api/query-range.ts
@@ -5,5 +5,3 @@ export const getQueryRangeUrl = (server: string, query: string, period: TimePara
 
 export const getQueryUrl = (server: string, query: string, period: TimeParams, queryTracing: boolean): string =>
   `${server}/api/v1/query?query=${encodeURIComponent(query)}&time=${period.end}${queryTracing ? "&trace=1" : ""}`;
-
-export const getQueryOptions = (server: string) => `${server}/api/v1/label/__name__/values`;
diff --git a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
index 78e3053f0c..4b3bb8b721 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
@@ -2,10 +2,11 @@ import React, { FC, useRef, useState } from "preact/compat";
 import { KeyboardEvent } from "react";
 import { ErrorTypes } from "../../../types";
 import TextField from "../../Main/TextField/TextField";
-import Autocomplete from "../../Main/Autocomplete/Autocomplete";
+import QueryEditorAutocomplete from "./QueryEditorAutocomplete";
 import "./style.scss";
 import { QueryStats } from "../../../api/types";
 import { partialWarning, seriesFetchedWarning } from "./warningText";
+import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
 
 export interface QueryEditorProps {
   onChange: (query: string) => void;
@@ -17,7 +18,6 @@ export interface QueryEditorProps {
   autocomplete: boolean;
   error?: ErrorTypes | string;
   stats?: QueryStats;
-  options: string[];
   label: string;
   disabled?: boolean
 }
@@ -31,13 +31,13 @@ const QueryEditor: FC<QueryEditorProps> = ({
   autocomplete,
   error,
   stats,
-  options,
   label,
   disabled = false
 }) => {
 
   const [openAutocomplete, setOpenAutocomplete] = useState(false);
-  const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
+  const [caretPosition, setCaretPosition] = useState([0, 0]);
+  const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
 
   const warning = [
     {
@@ -88,37 +88,43 @@ const QueryEditor: FC<QueryEditorProps> = ({
     }
   };
 
-  const handleChangeFoundOptions = (val: string[]) => {
+  const handleChangeFoundOptions = (val: AutocompleteOptions[]) => {
     setOpenAutocomplete(!!val.length);
   };
 
-  return <div
-    className="vm-query-editor"
-    ref={autocompleteAnchorEl}
-  >
-    <TextField
-      value={value}
-      label={label}
-      type={"textarea"}
-      autofocus={!!value}
-      error={error}
-      warning={warning}
-      onKeyDown={handleKeyDown}
-      onChange={onChange}
-      disabled={disabled}
-      inputmode={"search"}
-    />
-    {autocomplete && (
-      <Autocomplete
-        disabledFullScreen
+  const handleChangeCaret = (val: number[]) => {
+    setCaretPosition(val);
+  };
+
+  return (
+    <div
+      className="vm-query-editor"
+      ref={autocompleteAnchorEl}
+    >
+      <TextField
         value={value}
-        options={options}
-        anchor={autocompleteAnchorEl}
-        onSelect={handleSelect}
-        onFoundOptions={handleChangeFoundOptions}
+        label={label}
+        type={"textarea"}
+        autofocus={!!value}
+        error={error}
+        warning={warning}
+        onKeyDown={handleKeyDown}
+        onChange={onChange}
+        onChangeCaret={handleChangeCaret}
+        disabled={disabled}
+        inputmode={"search"}
       />
-    )}
-  </div>;
+      {autocomplete && (
+        <QueryEditorAutocomplete
+          value={value}
+          anchorEl={autocompleteAnchorEl}
+          caretPosition={caretPosition}
+          onSelect={handleSelect}
+          onFoundOptions={handleChangeFoundOptions}
+        />
+      )}
+    </div>
+  );
 };
 
 export default QueryEditor;
diff --git a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditorAutocomplete.tsx b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditorAutocomplete.tsx
new file mode 100644
index 0000000000..0bc30519a9
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditorAutocomplete.tsx
@@ -0,0 +1,129 @@
+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 useGetMetricsQL from "../../../hooks/useGetMetricsQL";
+
+enum ContextType {
+  empty = "empty",
+  metricsql = "metricsql",
+  label = "label",
+  value = "value",
+}
+
+interface QueryEditorAutocompleteProps {
+  value: string;
+  anchorEl: Ref<HTMLInputElement>;
+  caretPosition: number[];
+  onSelect: (val: string) => void;
+  onFoundOptions: (val: AutocompleteOptions[]) => void;
+}
+
+const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
+  value,
+  anchorEl,
+  caretPosition,
+  onSelect,
+  onFoundOptions
+}) => {
+  const [leftOffset, setLeftOffset] = useState(0);
+  const metricsqlFunctions = useGetMetricsQL();
+
+  const metric = useMemo(() => {
+    const regexp = /\b[^{}(),\s]+(?={|$)/g;
+    const match = value.match(regexp);
+    return match ? match[0] : "";
+  }, [value]);
+
+  const label = useMemo(() => {
+    const regexp = /[a-z_]\w*(?=\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]);
+
+  const { metrics, labels, values } = useFetchQueryOptions({ metric, label });
+
+  const options = useMemo(() => {
+    switch (context) {
+      case ContextType.metricsql:
+        return [...metrics, ...metricsqlFunctions];
+      case ContextType.label:
+        return labels;
+      case ContextType.value:
+        return values;
+      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]);
+
+  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));
+
+    if (context === ContextType.value) {
+      const quote = "\"";
+      const needsQuote = beforeInsert[beforeInsert.length - 1] !== quote;
+      insert = `${needsQuote ? quote : ""}${insert}${quote}`;
+    }
+
+    const newVal = `${beforeInsert}${insert}${afterInsert}`;
+    onSelect(newVal);
+  };
+
+  useEffect(() => {
+    if (!anchorEl.current) {
+      setLeftOffset(0);
+      return;
+    }
+
+    const style = window.getComputedStyle(anchorEl.current);
+    const fontSize = `${style.getPropertyValue("font-size")}`;
+    const fontFamily = `${style.getPropertyValue("font-family")}`;
+    const offset = getTextWidth(value, `${fontSize} ${fontFamily}`);
+    setLeftOffset(offset);
+  }, [anchorEl, caretPosition]);
+
+  return (
+    <Autocomplete
+      disabledFullScreen
+      value={valueByContext}
+      options={options}
+      anchor={anchorEl}
+      minLength={context === ContextType.metricsql ? 2 : 0}
+      offset={{ top: 0, left: leftOffset }}
+      onSelect={handleSelect}
+      onFoundOptions={onFoundOptions}
+    />
+  );
+};
+
+export default QueryEditorAutocomplete;
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 5f9a7d45f0..827340b506 100644
--- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
@@ -1,4 +1,4 @@
-import React, { FC, Ref, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
+import React, { FC, Ref, useCallback, useEffect, useMemo, useRef, useState, JSX } from "preact/compat";
 import classNames from "classnames";
 import Popper from "../Popper/Popper";
 import "./style.scss";
@@ -7,21 +7,33 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import useBoolean from "../../../hooks/useBoolean";
 import useEventListener from "../../../hooks/useEventListener";
 
+export interface AutocompleteOptions {
+  value: string;
+  description?: string;
+  type?: string;
+  icon?: JSX.Element
+}
+
 interface AutocompleteProps {
   value: string
-  options: string[]
+  options: AutocompleteOptions[]
   anchor: Ref<HTMLElement>
   disabled?: boolean
-  maxWords?: number
   minLength?: number
   fullWidth?: boolean
   noOptionsText?: string
   selected?: string[]
   label?: string
   disabledFullScreen?: boolean
+  offset?: {top: number, left: number}
   onSelect: (val: string) => void
   onOpenAutocomplete?: (val: boolean) => void
-  onFoundOptions?: (val: string[]) => void
+  onFoundOptions?: (val: AutocompleteOptions[]) => void
+}
+
+enum FocusType {
+  mouse,
+  keyboard
 }
 
 const Autocomplete: FC<AutocompleteProps> = ({
@@ -29,13 +41,13 @@ const Autocomplete: FC<AutocompleteProps> = ({
   options,
   anchor,
   disabled,
-  maxWords = 1,
   minLength = 2,
   fullWidth,
   selected,
   noOptionsText,
   label,
   disabledFullScreen,
+  offset,
   onSelect,
   onOpenAutocomplete,
   onFoundOptions
@@ -43,7 +55,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
   const { isMobile } = useDeviceDetect();
   const wrapperEl = useRef<HTMLDivElement>(null);
 
-  const [focusOption, setFocusOption] = useState(-1);
+  const [focusOption, setFocusOption] = useState<{index: number, type?: FocusType}>({ index: -1 });
 
   const {
     value: openAutocomplete,
@@ -54,9 +66,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
   const foundOptions = useMemo(() => {
     if (!openAutocomplete) return [];
     try {
-      const regexp = new RegExp(String(value), "i");
-      const found = options.filter((item) => regexp.test(item) && (item !== value));
-      return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
+      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));
     } catch (e) {
       return [];
     }
@@ -72,9 +84,17 @@ const Autocomplete: FC<AutocompleteProps> = ({
     if (!selected) handleCloseAutocomplete();
   };
 
+  const createHandlerMouseEnter = (index: number) => () => {
+    setFocusOption({ index, type: FocusType.mouse });
+  };
+
+  const handlerMouseLeave = () => {
+    setFocusOption({ index: -1 });
+  };
+
   const scrollToValue = () => {
-    if (!wrapperEl.current) return;
-    const target = wrapperEl.current.childNodes[focusOption] as HTMLElement;
+    if (!wrapperEl.current || focusOption.type === FocusType.mouse) return;
+    const target = wrapperEl.current.childNodes[focusOption.index] as HTMLElement;
     if (target?.scrollIntoView) target.scrollIntoView({ block: "center" });
   };
 
@@ -85,18 +105,24 @@ const Autocomplete: FC<AutocompleteProps> = ({
 
     if (key === "ArrowUp" && !modifiers && hasOptions) {
       e.preventDefault();
-      setFocusOption((prev) => prev <= 0 ? 0 : prev - 1);
+      setFocusOption(({ index }) => ({
+        index:  index <= 0 ? 0 : index - 1,
+        type: FocusType.keyboard
+      }));
     }
 
     if (key === "ArrowDown" && !modifiers && hasOptions) {
       e.preventDefault();
       const lastIndex = foundOptions.length - 1;
-      setFocusOption((prev) => prev >= lastIndex ? lastIndex : prev + 1);
+      setFocusOption(({ index }) => ({
+        index: index >= lastIndex ? lastIndex : index + 1,
+        type: FocusType.keyboard
+      }));
     }
 
     if (key === "Enter") {
-      const value = foundOptions[focusOption];
-      value && onSelect(value);
+      const item = foundOptions[focusOption.index];
+      item && onSelect(item.value);
       if (!selected) handleCloseAutocomplete();
     }
 
@@ -106,8 +132,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
   }, [focusOption, foundOptions, handleCloseAutocomplete, onSelect, selected]);
 
   useEffect(() => {
-    const words = (value.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
-    setOpenAutocomplete(value.length > minLength && words <= maxWords);
+    setOpenAutocomplete(value.length >= minLength);
   }, [value]);
 
   useEventListener("keydown", handleKeyDown);
@@ -115,7 +140,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
   useEffect(scrollToValue, [focusOption, foundOptions]);
 
   useEffect(() => {
-    setFocusOption(-1);
+    setFocusOption({ index: -1 });
   }, [foundOptions]);
 
   useEffect(() => {
@@ -135,6 +160,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
       fullWidth={fullWidth}
       title={isMobile ? label : undefined}
       disabledFullScreen={disabledFullScreen}
+      offset={offset}
     >
       <div
         className={classNames({
@@ -149,19 +175,34 @@ const Autocomplete: FC<AutocompleteProps> = ({
             className={classNames({
               "vm-list-item": true,
               "vm-list-item_mobile": isMobile,
-              "vm-list-item_active": i === focusOption,
+              "vm-list-item_active": i === focusOption.index,
               "vm-list-item_multiselect": selected,
-              "vm-list-item_multiselect_selected": selected?.includes(option)
+              "vm-list-item_multiselect_selected": selected?.includes(option.value),
+              "vm-list-item_with-icon":  option.icon,
             })}
-            id={`$autocomplete$${option}`}
-            key={option}
-            onClick={createHandlerSelect(option)}
+            id={`$autocomplete$${option.value}`}
+            key={`${i}${option.value}`}
+            onClick={createHandlerSelect(option.value)}
+            onMouseEnter={createHandlerMouseEnter(i)}
+            onMouseLeave={handlerMouseLeave}
           >
-            {selected?.includes(option) && <DoneIcon/>}
-            <span>{option}</span>
+            {selected?.includes(option.value) && <DoneIcon/>}
+            <>{option.icon}</>
+            <span>{option.value}</span>
           </div>
         )}
       </div>
+      {foundOptions[focusOption.index]?.description && (
+        <div className="vm-autocomplete-info">
+          <div className="vm-autocomplete-info__type">
+            {foundOptions[focusOption.index].type}
+          </div>
+          <div
+            className="vm-autocomplete-info__description"
+            dangerouslySetInnerHTML={{ __html: foundOptions[focusOption.index].description || "" }}
+          />
+        </div>
+      )}
     </Popper>
   );
 };
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 a1905058a2..d5d3f360e9 100644
--- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/style.scss
@@ -1,6 +1,7 @@
 @use "src/styles/variables" as *;
 
 .vm-autocomplete {
+  position: relative;
   max-height: 300px;
   overflow: auto;
   overscroll-behavior: none;
@@ -14,4 +15,34 @@
     text-align: center;
     color: $color-text-disabled;
   }
+
+  &-info {
+    position: absolute;
+    top: calc(100% + 1px);
+    left: 0;
+    right: 0;
+    min-width: 450px;
+    padding: $padding-global;
+    background-color: $color-background-block;
+    box-shadow: $box-shadow-popper;
+    border-radius: $border-radius-small;
+    overflow-wrap: anywhere;
+
+    &__type {
+      color: $color-text-secondary;
+      margin-bottom: $padding-small;
+    }
+
+    &__description {
+      line-height: 130%;
+
+      p {
+        margin: $padding-global 0;
+
+        &:last-child {
+          margin: 0;
+        }
+      }
+    }
+  }
 }
diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
index 4831ba7ec2..a3af70c69c 100644
--- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
@@ -1,4 +1,5 @@
 import React from "react";
+import { getCssVariable } from "../../../utils/theme";
 
 export const LogoIcon = () => (
   <svg
@@ -450,3 +451,53 @@ export const StarIcon = () => (
     <path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path>
   </svg>
 );
+
+export const MetricIcon = () => (
+  <svg
+    viewBox="0 0 16 16"
+    fill={getCssVariable("color-error")}
+  >
+    <path
+      d="M13.5095 4L8.50952 1H7.50952L2.50952 4L2.01953 4.85999V10.86L2.50952 11.71L7.50952 14.71H8.50952L13.5095 11.71L13.9995 10.86V4.85999L13.5095 4ZM7.50952 13.5601L3.00952 10.86V5.69995L7.50952 8.15002V13.5601ZM3.26953 4.69995L8.00952 1.85999L12.7495 4.69995L8.00952 7.29004L3.26953 4.69995ZM13.0095 10.86L8.50952 13.5601V8.15002L13.0095 5.69995V10.86Z"
+    />
+  </svg>
+);
+
+export const FunctionIcon = () => (
+  <svg
+    viewBox="0 0 16 16"
+    fill={getCssVariable("color-primary")}
+  >
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M2 5H4V4H1.5L1 4.5V12.5L1.5 13H4V12H2V5ZM14.5 4H12V5H14V12H12V13H14.5L15 12.5V4.5L14.5 4ZM11.76 6.56995L12 7V9.51001L11.7 9.95996L7.19995 11.96H6.73999L4.23999 10.46L4 10.03V7.53003L4.30005 7.06995L8.80005 5.06995H9.26001L11.76 6.56995ZM5 9.70996L6.5 10.61V9.28003L5 8.38V9.70996ZM5.57996 7.56006L7.03003 8.43005L10.42 6.93005L8.96997 6.06006L5.57996 7.56006ZM7.53003 10.73L11.03 9.17004V7.77002L7.53003 9.31995V10.73Z"
+    />
+  </svg>
+);
+
+export const LabelIcon = () => (
+  <svg
+    viewBox="0 0 16 16"
+    fill={getCssVariable("color-warning")}
+  >
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M14 2H8L7 3V6H8V3H14V8H10V9H14L15 8V3L14 2ZM9 6H13V7H9.41L9 6.59V6ZM7 7H2L1 8V13L2 14H8L9 13V8L8 7H7ZM8 13H2V8H8V9V13ZM3 9H7V10H3V9ZM3 11H7V12H3V11ZM9 4H13V5H9V4Z"
+    />
+  </svg>
+);
+
+export const ValueIcon = () => (
+  <svg
+    viewBox="0 0 16 16"
+    fill={getCssVariable("color-primary")}
+  >
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M7 3L8 2H14L15 3V8L14 9H10V8H14V3H8V6H7V3ZM9 9V8L8 7H7H2L1 8V13L2 14H8L9 13V9ZM8 8V9V13H2V8H7H8ZM9.41421 7L9 6.58579V6H13V7H9.41421ZM9 4H13V5H9V4ZM7 10H3V11H7V10Z"
+    />
+  </svg>
+);
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
index 906df1a035..4d93eddaac 100644
--- a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
@@ -160,11 +160,10 @@ const Select: FC<SelectProps> = ({
       <Autocomplete
         label={label}
         value={autocompleteValue}
-        options={list}
+        options={list.map(el => ({ value: el }))}
         anchor={autocompleteAnchorEl}
         selected={selectedValues}
-        maxWords={10}
-        minLength={0}
+        minLength={1}
         fullWidth
         noOptionsText={noOptionsText}
         onSelect={handleSelected}
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 7e6211e3bb..ce1b21caad 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
@@ -1,6 +1,15 @@
-import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, ReactNode } from "react";
+import React, {
+  FC,
+  useEffect,
+  useRef,
+  useMemo,
+  FormEvent,
+  KeyboardEvent,
+  MouseEvent,
+  HTMLInputTypeAttribute,
+  ReactNode
+} from "react";
 import classNames from "classnames";
-import { useMemo } from "preact/compat";
 import { useAppState } from "../../../state/common/StateContext";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import TextFieldMessage from "./TextFieldMessage";
@@ -24,6 +33,7 @@ interface TextFieldProps {
   onKeyDown?: (e: KeyboardEvent) => void
   onFocus?: () => void
   onBlur?: () => void
+  onChangeCaret?: (position: number[]) => void
 }
 
 const TextField: FC<TextFieldProps> = ({
@@ -43,7 +53,8 @@ const TextField: FC<TextFieldProps> = ({
   onEnter,
   onKeyDown,
   onFocus,
-  onBlur
+  onBlur,
+  onChangeCaret,
 }) => {
   const { isDarkTheme } = useAppState();
   const { isMobile } = useDeviceDetect();
@@ -61,9 +72,18 @@ const TextField: FC<TextFieldProps> = ({
     "vm-text-field__input_textarea": type === "textarea",
   });
 
+  const updateCaretPosition = (target: HTMLInputElement | HTMLTextAreaElement) => {
+    const { selectionStart, selectionEnd } = target;
+    onChangeCaret && onChangeCaret([selectionStart || 0, selectionEnd || 0]);
+  };
+
+  const handleMouseUp = (e: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+    updateCaretPosition(e.currentTarget);
+  };
+
   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);
@@ -73,9 +93,10 @@ const TextField: FC<TextFieldProps> = ({
     }
   };
 
-  const handleChange = (e: React.FormEvent) => {
+  const handleChange = (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
     if (disabled) return;
-    onChange && onChange((e.target as HTMLInputElement).value);
+    onChange && onChange(e.currentTarget.value);
+    updateCaretPosition(e.currentTarget);
   };
 
   useEffect(() => {
@@ -116,6 +137,7 @@ const TextField: FC<TextFieldProps> = ({
           onKeyDown={handleKeyDown}
           onFocus={handleFocus}
           onBlur={handleBlur}
+          onMouseUp={handleMouseUp}
         />
       )
       : (
@@ -132,6 +154,7 @@ const TextField: FC<TextFieldProps> = ({
           onKeyDown={handleKeyDown}
           onFocus={handleFocus}
           onBlur={handleBlur}
+          onMouseUp={handleMouseUp}
         />
       )
     }
diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.ts b/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.ts
deleted file mode 100644
index 456ae367cc..0000000000
--- a/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useEffect, useState } from "preact/compat";
-import { getQueryOptions } from "../api/query-range";
-import { useAppState } from "../state/common/StateContext";
-
-export const useFetchQueryOptions = (): {
-  queryOptions: string[],
-} => {
-  const { serverUrl } = useAppState();
-
-  const [queryOptions, setQueryOptions] = useState([]);
-
-  const fetchOptions = async () => {
-    if (!serverUrl) return;
-    const url = getQueryOptions(serverUrl);
-
-    try {
-      const response = await fetch(url);
-      const resp = await response.json();
-      if (response.ok) {
-        setQueryOptions(resp.data);
-      }
-    } catch (e) {
-      console.error(e);
-    }
-  };
-
-  useEffect(() => {
-    fetchOptions();
-  }, [serverUrl]);
-
-  return { queryOptions };
-};
diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx b/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
new file mode 100644
index 0000000000..1cd028058b
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/useFetchQueryOptions.tsx
@@ -0,0 +1,94 @@
+import React, { StateUpdater, useEffect, useState } from "preact/compat";
+import { useAppState } from "../state/common/StateContext";
+import { AutocompleteOptions } from "../components/Main/Autocomplete/Autocomplete";
+import { LabelIcon, MetricIcon, ValueIcon } from "../components/Main/Icons";
+
+enum TypeData {
+  metric,
+  label,
+  value
+}
+
+type FetchDataArgs = {
+  url: string;
+  setter: StateUpdater<AutocompleteOptions[]>;
+  type: TypeData;
+}
+
+const icons = {
+  [TypeData.metric]: <MetricIcon />,
+  [TypeData.label]: <LabelIcon />,
+  [TypeData.value]: <ValueIcon />,
+};
+
+export const useFetchQueryOptions = ({ metric, label }: { metric: string; label: string }) => {
+  const { serverUrl } = useAppState();
+
+  const [metrics, setMetrics] = useState<AutocompleteOptions[]>([]);
+  const [labels, setLabels] = useState<AutocompleteOptions[]>([]);
+  const [values, setValues] = useState<AutocompleteOptions[]>([]);
+
+  const fetchData = async ({ url, setter, type, }: FetchDataArgs) => {
+    try {
+      const response = await fetch(url);
+      if (response.ok) {
+        const { data } = await response.json() as { data: string[] };
+        setter(data.map(l => ({
+          value: l,
+          type: `${type}`,
+          icon: icons[type]
+        })));
+      }
+    } catch (e) {
+      console.error(e);
+    }
+  };
+
+  useEffect(() => {
+    if (!serverUrl) {
+      setMetrics([]);
+      return;
+    }
+
+    fetchData({
+      url: `${serverUrl}/api/v1/label/__name__/values`,
+      setter: setMetrics,
+      type: TypeData.metric
+    });
+  }, [serverUrl]);
+
+  useEffect(() => {
+    const notFoundMetric = !metrics.find(m => m.value === metric);
+    if (!serverUrl || notFoundMetric) {
+      setLabels([]);
+      return;
+    }
+
+    fetchData({
+      url: `${serverUrl}/api/v1/labels?match[]=${metric}`,
+      setter: setLabels,
+      type: TypeData.label
+    });
+  }, [serverUrl, metric]);
+
+  useEffect(() => {
+    const notFoundMetric = !metrics.find(m => m.value === metric);
+    const notFoundLabel = !labels.find(l => l.value === label);
+    if (!serverUrl || notFoundMetric || notFoundLabel) {
+      setValues([]);
+      return;
+    }
+
+    fetchData({
+      url: `${serverUrl}/api/v1/label/${label}/values?match[]=${metric}`,
+      setter: setValues,
+      type: TypeData.value
+    });
+  }, [serverUrl, metric, label]);
+
+  return {
+    metrics,
+    labels,
+    values,
+  };
+};
diff --git a/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx b/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
new file mode 100644
index 0000000000..fbe3960f4a
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/useGetMetricsQL.tsx
@@ -0,0 +1,78 @@
+import React, { useEffect, useState } 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";
+
+const CATEGORY_TAG = "h3";
+const FUNCTION_TAG = "h4";
+const DESCRIPTION_TAG = "p";
+
+const docsUrl = "https://docs.victoriametrics.com/MetricsQL.html";
+const classLink = "vm-link vm-link_colored";
+
+const prepareDescription = (text: string): string => {
+  const replaceValue = `$1 target="_blank" class="${classLink}" $2${docsUrl}#`;
+  return text.replace(/(<a) (href=")#/gm, replaceValue);
+};
+
+const getParagraph = (el: Element): Element[] => {
+  const paragraphs: Element[] = [];
+  let nextEl = el.nextElementSibling;
+  while (nextEl && nextEl.tagName.toLowerCase() === DESCRIPTION_TAG) {
+    if (nextEl) paragraphs.push(nextEl);
+    nextEl = nextEl.nextElementSibling;
+  }
+  return paragraphs;
+};
+
+const createAutocompleteOption = (type: string, group: Element): AutocompleteOptions => {
+  const value = group.textContent ?? "";
+  const paragraphs = getParagraph(group);
+  const description = paragraphs.map(p => p.outerHTML ?? "").join("\n");
+  return {
+    type,
+    value,
+    description: prepareDescription(description),
+    icon: <FunctionIcon />,
+  };
+};
+
+const processGroups = (groups: NodeListOf<Element>): AutocompleteOptions[] => {
+  let type = "";
+  return Array.from(groups).map(group => {
+    const isCategory = group.tagName.toLowerCase() === CATEGORY_TAG;
+    type = isCategory ? group.textContent ?? "" : type;
+    return isCategory ? null : createAutocompleteOption(type, group);
+  }).filter(Boolean) as AutocompleteOptions[];
+};
+
+const useGetMetricsQL = () => {
+  const [metricsQLFunctions, setMetricsQLFunctions] = useState<AutocompleteOptions[]>([]);
+
+  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);
+  };
+
+  useEffect(() => {
+    const fetchMarkdown = async () => {
+      try {
+        const resp = await fetch(MetricsQL);
+        const text = await resp.text();
+        processMarkdown(text);
+      } catch (e) {
+        console.error("Error fetching or processing the MetricsQL.md file:", e);
+      }
+    };
+
+    fetchMarkdown();
+  }, []);
+
+  return metricsQLFunctions;
+};
+
+export default useGetMetricsQL;
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 7648ffc0b1..312fe4ec34 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
@@ -29,7 +29,6 @@ export interface QueryConfiguratorProps {
   setQueryErrors: StateUpdater<string[]>;
   setHideError: StateUpdater<boolean>;
   stats: QueryStats[];
-  queryOptions: string[]
   onHideQuery: (queries: number[]) => void
   onRunQuery: () => void
 }
@@ -39,7 +38,6 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
   setQueryErrors,
   setHideError,
   stats,
-  queryOptions,
   onHideQuery,
   onRunQuery
 }) => {
@@ -189,7 +187,6 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
           <QueryEditor
             value={stateQuery[i]}
             autocomplete={autocomplete}
-            options={queryOptions}
             error={queryErrors[i]}
             stats={stats[i]}
             onArrowUp={createHandlerArrow(-1, i)}
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
index fb170f9bd7..795ef78e69 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
@@ -8,7 +8,6 @@ import GraphSettings from "../../components/Configurators/GraphSettings/GraphSet
 import { useGraphDispatch, useGraphState } from "../../state/graph/GraphStateContext";
 import { AxisRange } from "../../state/graph/reducer";
 import Spinner from "../../components/Main/Spinner/Spinner";
-import { useFetchQueryOptions } from "../../hooks/useFetchQueryOptions";
 import TracingsView from "../../components/TraceQuery/TracingsView";
 import Trace from "../../components/TraceQuery/Trace";
 import TableSettings from "../../components/Table/TableSettings/TableSettings";
@@ -50,7 +49,6 @@ const CustomPanel: FC = () => {
   const { customStep, yaxis } = useGraphState();
   const graphDispatch = useGraphDispatch();
 
-  const { queryOptions } = useFetchQueryOptions();
   const {
     isLoading,
     liveData,
@@ -133,7 +131,6 @@ const CustomPanel: FC = () => {
         setQueryErrors={setQueryErrors}
         setHideError={setHideError}
         stats={queryStats}
-        queryOptions={queryOptions}
         onHideQuery={handleHideQuery}
         onRunQuery={handleRunQuery}
       />
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsHeader/ExploreLogsHeader.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsHeader/ExploreLogsHeader.tsx
index 92f7480aa1..0d51f1224e 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsHeader/ExploreLogsHeader.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsHeader/ExploreLogsHeader.tsx
@@ -27,7 +27,6 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, onChange, onRun }
         <QueryEditor
           value={query}
           autocomplete={false}
-          options={[]}
           onArrowUp={() => null}
           onArrowDown={() => null}
           onEnter={onRun}
diff --git a/app/vmui/packages/vmui/src/styles/components/list.scss b/app/vmui/packages/vmui/src/styles/components/list.scss
index 8dd60bb5f4..9fc9f58496 100644
--- a/app/vmui/packages/vmui/src/styles/components/list.scss
+++ b/app/vmui/packages/vmui/src/styles/components/list.scss
@@ -35,5 +35,13 @@
         color: $color-primary;
       }
     }
+
+    &_with-icon {
+      display: grid;
+      grid-template-columns: 14px 1fr;
+      gap: calc($padding-small/2);
+      align-items: center;
+      justify-content: flex-start;
+    }
   }
 }
diff --git a/app/vmui/packages/vmui/src/types/markdown.d.ts b/app/vmui/packages/vmui/src/types/markdown.d.ts
new file mode 100644
index 0000000000..d3f4b12bce
--- /dev/null
+++ b/app/vmui/packages/vmui/src/types/markdown.d.ts
@@ -0,0 +1,4 @@
+declare module "*.md" {
+  const value: string;
+  export default value;
+}
diff --git a/app/vmui/packages/vmui/src/utils/regexp.ts b/app/vmui/packages/vmui/src/utils/regexp.ts
new file mode 100644
index 0000000000..96e6f859c9
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/regexp.ts
@@ -0,0 +1,3 @@
+export const escapeRegExp = (str: string) => {
+  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+};
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index dfbfc72dc6..112af249f9 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -31,6 +31,7 @@ The sandbox cluster installation is running under the constant load generated by
 * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): support data ingestion from [NewRelic infrastructure agent](https://docs.newrelic.com/docs/infrastructure/install-infrastructure-agent). See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-newrelic-agent), [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3520) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4712).
 * FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup.html): add `-filestream.disableFadvise` command-line flag, which can be used for disabling `fadvise` syscall during backup upload to the remote storage. By default `vmbackup` uses `fadvise` syscall in order to prevent from eviction of recently accessed data from the [OS page cache](https://en.wikipedia.org/wiki/Page_cache) when backing up large files. Sometimes the `fadvise` syscall may take significant amounts of CPU when the backup is performed with large value of `-concurrency` command-line flag on systems with big number of CPU cores. In this case it is better to manually disable `fadvise` syscall by passing `-filestream.disableFadvise` command-line flag to `vmbackup`. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5120) for details.
 * FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): account for `vmauth` component for alerts `ServiceDown` and `TooManyRestarts`.
+* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add support for functions, labels, values in autocomplete. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3006).
 
 ## [v1.94.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.94.0)