mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-01 14:47:38 +00:00
vmui: trigger auto-suggestion at any cursor position (#6155)
- Implemented auto-suggestion triggers for mid-string cursor positions in vmui. - Improved the suggestion list positioning to appear directly beneath the active text editing area. https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5864
This commit is contained in:
parent
6aaf1768f4
commit
6193fa3dcf
8 changed files with 131 additions and 41 deletions
|
@ -9,6 +9,7 @@ import { partialWarning, seriesFetchedWarning } from "./warningText";
|
||||||
import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
import { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import { useQueryState } from "../../../state/query/QueryStateContext";
|
import { useQueryState } from "../../../state/query/QueryStateContext";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
export interface QueryEditorProps {
|
export interface QueryEditorProps {
|
||||||
onChange: (query: string) => void;
|
onChange: (query: string) => void;
|
||||||
|
@ -40,9 +41,12 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
||||||
const [caretPosition, setCaretPosition] = useState([0, 0]);
|
const [caretPosition, setCaretPosition] = useState<[number, number]>([0, 0]);
|
||||||
const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
|
const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [showAutocomplete, setShowAutocomplete] = useState(autocomplete);
|
||||||
|
const debouncedSetShowAutocomplete = useRef(debounce(setShowAutocomplete, 500)).current;
|
||||||
|
|
||||||
const warning = [
|
const warning = [
|
||||||
{
|
{
|
||||||
show: stats?.seriesFetched === "0" && !stats.resultLength,
|
show: stats?.seriesFetched === "0" && !stats.resultLength,
|
||||||
|
@ -58,8 +62,9 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
label = `${label} (${stats.executionTimeMsec || 0}ms)`;
|
label = `${label} (${stats.executionTimeMsec || 0}ms)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (val: string) => {
|
const handleSelect = (val: string, caretPosition: number) => {
|
||||||
onChange(val);
|
onChange(val);
|
||||||
|
setCaretPosition([caretPosition, caretPosition]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
@ -100,14 +105,19 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
setOpenAutocomplete(!!val.length);
|
setOpenAutocomplete(!!val.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeCaret = (val: number[]) => {
|
const handleChangeCaret = (val: [number, number]) => {
|
||||||
setCaretPosition(val);
|
setCaretPosition(prev => prev[0] === val[0] && prev[1] === val[1] ? prev : val);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpenAutocomplete(autocomplete);
|
setOpenAutocomplete(autocomplete);
|
||||||
}, [autocompleteQuick]);
|
}, [autocompleteQuick]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowAutocomplete(false);
|
||||||
|
debouncedSetShowAutocomplete(true);
|
||||||
|
}, [caretPosition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="vm-query-editor"
|
className="vm-query-editor"
|
||||||
|
@ -125,12 +135,14 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
onChangeCaret={handleChangeCaret}
|
onChangeCaret={handleChangeCaret}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputmode={"search"}
|
inputmode={"search"}
|
||||||
|
caretPosition={caretPosition}
|
||||||
/>
|
/>
|
||||||
{autocomplete && (
|
{showAutocomplete && autocomplete && (
|
||||||
<QueryEditorAutocomplete
|
<QueryEditorAutocomplete
|
||||||
value={value}
|
value={value}
|
||||||
anchorEl={autocompleteAnchorEl}
|
anchorEl={autocompleteAnchorEl}
|
||||||
caretPosition={caretPosition}
|
caretPosition={caretPosition}
|
||||||
|
hasHelperText={Boolean(warning || error)}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onFoundOptions={handleChangeFoundOptions}
|
onFoundOptions={handleChangeFoundOptions}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { FC, Ref, useState, useEffect, useMemo } from "preact/compat";
|
import React, { FC, Ref, useState, useEffect, useMemo, useCallback } from "preact/compat";
|
||||||
import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
||||||
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
||||||
import { getTextWidth } from "../../../utils/uplot";
|
|
||||||
import { escapeRegexp, hasUnclosedQuotes } from "../../../utils/regexp";
|
import { escapeRegexp, hasUnclosedQuotes } from "../../../utils/regexp";
|
||||||
import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
|
import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
|
||||||
import { QueryContextType } from "../../../types";
|
import { QueryContextType } from "../../../types";
|
||||||
|
@ -10,8 +9,9 @@ import { AUTOCOMPLETE_LIMITS } from "../../../constants/queryAutocomplete";
|
||||||
interface QueryEditorAutocompleteProps {
|
interface QueryEditorAutocompleteProps {
|
||||||
value: string;
|
value: string;
|
||||||
anchorEl: Ref<HTMLInputElement>;
|
anchorEl: Ref<HTMLInputElement>;
|
||||||
caretPosition: number[];
|
caretPosition: [number, number]; // [start, end]
|
||||||
onSelect: (val: string) => void;
|
hasHelperText: boolean;
|
||||||
|
onSelect: (val: string, caretPosition: number) => void;
|
||||||
onFoundOptions: (val: AutocompleteOptions[]) => void;
|
onFoundOptions: (val: AutocompleteOptions[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,16 +19,24 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
value,
|
value,
|
||||||
anchorEl,
|
anchorEl,
|
||||||
caretPosition,
|
caretPosition,
|
||||||
|
hasHelperText,
|
||||||
onSelect,
|
onSelect,
|
||||||
onFoundOptions
|
onFoundOptions
|
||||||
}) => {
|
}) => {
|
||||||
const [leftOffset, setLeftOffset] = useState(0);
|
const [offsetPos, setOffsetPos] = useState({ top: 0, left: 0 });
|
||||||
const metricsqlFunctions = useGetMetricsQL();
|
const metricsqlFunctions = useGetMetricsQL();
|
||||||
|
|
||||||
|
const values = useMemo(() => {
|
||||||
|
if (caretPosition[0] !== caretPosition[1]) return { beforeCursor: value, afterCursor: "" };
|
||||||
|
const beforeCursor = value.substring(0, caretPosition[0]);
|
||||||
|
const afterCursor = value.substring(caretPosition[1]);
|
||||||
|
return { beforeCursor, afterCursor };
|
||||||
|
}, [value, caretPosition]);
|
||||||
|
|
||||||
const exprLastPart = useMemo(() => {
|
const exprLastPart = useMemo(() => {
|
||||||
const parts = value.split("}");
|
const parts = values.beforeCursor.split("}");
|
||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1];
|
||||||
}, [value]);
|
}, [values]);
|
||||||
|
|
||||||
const metric = useMemo(() => {
|
const metric = useMemo(() => {
|
||||||
const regexp = /\b[^{}(),\s]+(?={|$)/g;
|
const regexp = /\b[^{}(),\s]+(?={|$)/g;
|
||||||
|
@ -43,7 +51,7 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
}, [exprLastPart]);
|
}, [exprLastPart]);
|
||||||
|
|
||||||
const shouldSuppressAutoSuggestion = (value: string) => {
|
const shouldSuppressAutoSuggestion = (value: string) => {
|
||||||
const pattern = /([(),+\-*/^]|\b(?:or|and|unless|default|ifnot|if|group_left|group_right)\b)/;
|
const pattern = /([{(),+\-*/^]|\b(?:or|and|unless|default|ifnot|if|group_left|group_right)\b)/;
|
||||||
const parts = value.split(/\s+/);
|
const parts = value.split(/\s+/);
|
||||||
const partsCount = parts.length;
|
const partsCount = parts.length;
|
||||||
const lastPart = parts[partsCount - 1];
|
const lastPart = parts[partsCount - 1];
|
||||||
|
@ -55,7 +63,7 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const context = useMemo(() => {
|
const context = useMemo(() => {
|
||||||
if (!value || value.endsWith("}") || shouldSuppressAutoSuggestion(value)) {
|
if (!values.beforeCursor || values.beforeCursor.endsWith("}") || shouldSuppressAutoSuggestion(values.beforeCursor)) {
|
||||||
return QueryContextType.empty;
|
return QueryContextType.empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,19 +71,19 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
const labelValueRegexp = new RegExp(`(${escapeRegexp(metric)})?{?.+${escapeRegexp(label)}(=|!=|=~|!~)"?([^"]*)$`, "g");
|
const labelValueRegexp = new RegExp(`(${escapeRegexp(metric)})?{?.+${escapeRegexp(label)}(=|!=|=~|!~)"?([^"]*)$`, "g");
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case labelValueRegexp.test(value):
|
case labelValueRegexp.test(values.beforeCursor):
|
||||||
return QueryContextType.labelValue;
|
return QueryContextType.labelValue;
|
||||||
case labelRegexp.test(value):
|
case labelRegexp.test(values.beforeCursor):
|
||||||
return QueryContextType.label;
|
return QueryContextType.label;
|
||||||
default:
|
default:
|
||||||
return QueryContextType.metricsql;
|
return QueryContextType.metricsql;
|
||||||
}
|
}
|
||||||
}, [value, metric, label]);
|
}, [values, metric, label]);
|
||||||
|
|
||||||
const valueByContext = useMemo(() => {
|
const valueByContext = useMemo(() => {
|
||||||
const wordMatch = value.match(/([\w_\-.:/]+(?![},]))$/);
|
const wordMatch = values.beforeCursor.match(/([\w_\-.:/]+(?![},]))$/);
|
||||||
return wordMatch ? wordMatch[0] : "";
|
return wordMatch ? wordMatch[0] : "";
|
||||||
}, [value]);
|
}, [values.beforeCursor]);
|
||||||
|
|
||||||
const { metrics, labels, labelValues, loading } = useFetchQueryOptions({
|
const { metrics, labels, labelValues, loading } = useFetchQueryOptions({
|
||||||
valueByContext,
|
valueByContext,
|
||||||
|
@ -97,8 +105,10 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
}
|
}
|
||||||
}, [context, metrics, labels, labelValues]);
|
}, [context, metrics, labels, labelValues]);
|
||||||
|
|
||||||
const handleSelect = (insert: string) => {
|
const handleSelect = useCallback((insert: string) => {
|
||||||
// Find the start and end of valueByContext in the query string
|
// Find the start and end of valueByContext in the query string
|
||||||
|
const value = values.beforeCursor;
|
||||||
|
let valueAfterCursor = values.afterCursor;
|
||||||
const startIndexOfValueByContext = value.lastIndexOf(valueByContext, caretPosition[0]);
|
const startIndexOfValueByContext = value.lastIndexOf(valueByContext, caretPosition[0]);
|
||||||
const endIndexOfValueByContext = startIndexOfValueByContext + valueByContext.length;
|
const endIndexOfValueByContext = startIndexOfValueByContext + valueByContext.length;
|
||||||
|
|
||||||
|
@ -110,26 +120,59 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
if (context === QueryContextType.labelValue) {
|
if (context === QueryContextType.labelValue) {
|
||||||
const quote = "\"";
|
const quote = "\"";
|
||||||
const needsQuote = /(?:=|!=|=~|!~)$/.test(beforeValueByContext);
|
const needsQuote = /(?:=|!=|=~|!~)$/.test(beforeValueByContext);
|
||||||
|
valueAfterCursor = valueAfterCursor.replace(/^[^\s"|},]*/, "");
|
||||||
insert = `${needsQuote ? quote : ""}${insert}`;
|
insert = `${needsQuote ? quote : ""}${insert}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context === QueryContextType.label) {
|
||||||
|
valueAfterCursor = valueAfterCursor.replace(/^[^\s=!,{}()"|+\-/*^]*/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context === QueryContextType.metricsql) {
|
||||||
|
valueAfterCursor = valueAfterCursor.replace(/^[^\s[\]{}()"|+\-/*^]*/, "");
|
||||||
|
}
|
||||||
// Assemble the new value with the inserted text
|
// Assemble the new value with the inserted text
|
||||||
const newVal = `${beforeValueByContext}${insert}${afterValueByContext}`;
|
const newVal = `${beforeValueByContext}${insert}${afterValueByContext}${valueAfterCursor}`;
|
||||||
onSelect(newVal);
|
onSelect(newVal, beforeValueByContext.length + insert.length);
|
||||||
};
|
}, [values]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!anchorEl.current) {
|
if (!anchorEl.current) {
|
||||||
setLeftOffset(0);
|
setOffsetPos({ top: 0, left: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = window.getComputedStyle(anchorEl.current);
|
const element = anchorEl.current.querySelector("textarea") || anchorEl.current;
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
const fontSize = `${style.getPropertyValue("font-size")}`;
|
const fontSize = `${style.getPropertyValue("font-size")}`;
|
||||||
const fontFamily = `${style.getPropertyValue("font-family")}`;
|
const fontFamily = `${style.getPropertyValue("font-family")}`;
|
||||||
const offset = getTextWidth(value, `${fontSize} ${fontFamily}`);
|
const lineHeight = parseInt(`${style.getPropertyValue("line-height")}`);
|
||||||
setLeftOffset(offset);
|
|
||||||
}, [anchorEl, caretPosition]);
|
const span = document.createElement("div");
|
||||||
|
span.style.font = `${fontSize} ${fontFamily}`;
|
||||||
|
span.style.padding = style.getPropertyValue("padding");
|
||||||
|
span.style.lineHeight = `${lineHeight}px`;
|
||||||
|
span.style.width = `${element.offsetWidth}px`;
|
||||||
|
span.style.maxWidth = `${element.offsetWidth}px`;
|
||||||
|
span.style.whiteSpace = style.getPropertyValue("white-space");
|
||||||
|
span.style.overflowWrap = style.getPropertyValue("overflow-wrap");
|
||||||
|
|
||||||
|
const marker = document.createElement("span");
|
||||||
|
span.appendChild(document.createTextNode(values.beforeCursor));
|
||||||
|
span.appendChild(marker);
|
||||||
|
span.appendChild(document.createTextNode(values.afterCursor));
|
||||||
|
document.body.appendChild(span);
|
||||||
|
|
||||||
|
const spanRect = span.getBoundingClientRect();
|
||||||
|
const markerRect = marker.getBoundingClientRect();
|
||||||
|
|
||||||
|
const leftOffset = markerRect.left - spanRect.left;
|
||||||
|
const topOffset = markerRect.bottom - spanRect.bottom - (hasHelperText ? lineHeight : 0);
|
||||||
|
setOffsetPos({ top: topOffset, left: leftOffset });
|
||||||
|
|
||||||
|
span.remove();
|
||||||
|
marker.remove();
|
||||||
|
}, [anchorEl, caretPosition, hasHelperText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -140,7 +183,7 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||||
options={options}
|
options={options}
|
||||||
anchor={anchorEl}
|
anchor={anchorEl}
|
||||||
minLength={0}
|
minLength={0}
|
||||||
offset={{ top: 0, left: leftOffset }}
|
offset={offsetPos}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onFoundOptions={onFoundOptions}
|
onFoundOptions={onFoundOptions}
|
||||||
maxDisplayResults={{
|
maxDisplayResults={{
|
||||||
|
|
|
@ -2,4 +2,13 @@
|
||||||
|
|
||||||
.vm-query-editor {
|
.vm-query-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.marker-detection {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -9999;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
const { key, ctrlKey, metaKey, shiftKey } = e;
|
const { key, ctrlKey, metaKey, shiftKey } = e;
|
||||||
const modifiers = ctrlKey || metaKey || shiftKey;
|
const modifiers = ctrlKey || metaKey || shiftKey;
|
||||||
const hasOptions = foundOptions.length;
|
const hasOptions = foundOptions.length && !hideFoundedOptions;
|
||||||
|
|
||||||
if (key === "ArrowUp" && !modifiers && hasOptions) {
|
if (key === "ArrowUp" && !modifiers && hasOptions) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -148,7 +148,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
if (key === "Escape") {
|
if (key === "Escape") {
|
||||||
handleCloseAutocomplete();
|
handleCloseAutocomplete();
|
||||||
}
|
}
|
||||||
}, [focusOption, foundOptions, handleCloseAutocomplete, onSelect, selected]);
|
}, [focusOption, foundOptions, hideFoundedOptions, handleCloseAutocomplete, onSelect, selected]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpenAutocomplete(value.length >= minLength);
|
setOpenAutocomplete(value.length >= minLength);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {
|
import React, {
|
||||||
FC,
|
FC,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
FormEvent,
|
FormEvent,
|
||||||
|
@ -28,12 +29,13 @@ interface TextFieldProps {
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
helperText?: string
|
helperText?: string
|
||||||
inputmode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"
|
inputmode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"
|
||||||
|
caretPosition?: [number, number]
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
onEnter?: () => void
|
onEnter?: () => void
|
||||||
onKeyDown?: (e: KeyboardEvent) => void
|
onKeyDown?: (e: KeyboardEvent) => void
|
||||||
onFocus?: () => void
|
onFocus?: () => void
|
||||||
onBlur?: () => void
|
onBlur?: () => void
|
||||||
onChangeCaret?: (position: number[]) => void
|
onChangeCaret?: (position: [number, number]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextField: FC<TextFieldProps> = ({
|
const TextField: FC<TextFieldProps> = ({
|
||||||
|
@ -49,6 +51,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
inputmode = "text",
|
inputmode = "text",
|
||||||
|
caretPosition,
|
||||||
onChange,
|
onChange,
|
||||||
onEnter,
|
onEnter,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
@ -62,6 +65,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fieldRef = useMemo(() => type === "textarea" ? textareaRef : inputRef, [type]);
|
const fieldRef = useMemo(() => type === "textarea" ? textareaRef : inputRef, [type]);
|
||||||
|
const [selectionPos, setSelectionPos] = useState<[start: number, end: number]>([0, 0]);
|
||||||
|
|
||||||
const inputClasses = classNames({
|
const inputClasses = classNames({
|
||||||
"vm-text-field__input": true,
|
"vm-text-field__input": true,
|
||||||
|
@ -74,7 +78,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
|
|
||||||
const updateCaretPosition = (target: HTMLInputElement | HTMLTextAreaElement) => {
|
const updateCaretPosition = (target: HTMLInputElement | HTMLTextAreaElement) => {
|
||||||
const { selectionStart, selectionEnd } = target;
|
const { selectionStart, selectionEnd } = target;
|
||||||
onChangeCaret && onChangeCaret([selectionStart || 0, selectionEnd || 0]);
|
setSelectionPos([selectionStart || 0, selectionEnd || 0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = (e: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleMouseUp = (e: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
@ -102,11 +106,6 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
updateCaretPosition(e.currentTarget);
|
updateCaretPosition(e.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!autofocus || isMobile) return;
|
|
||||||
fieldRef?.current?.focus && fieldRef.current.focus();
|
|
||||||
}, [fieldRef, autofocus]);
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
onFocus && onFocus();
|
onFocus && onFocus();
|
||||||
};
|
};
|
||||||
|
@ -115,6 +114,31 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
onBlur && onBlur();
|
onBlur && onBlur();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectionRange = (range: [number, number]) => {
|
||||||
|
try {
|
||||||
|
fieldRef.current && fieldRef.current.setSelectionRange(range[0], range[1]);
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autofocus || isMobile) return;
|
||||||
|
fieldRef?.current?.focus && fieldRef.current.focus();
|
||||||
|
}, [fieldRef, autofocus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeCaret && onChangeCaret(selectionPos);
|
||||||
|
}, [selectionPos]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectionRange(selectionPos);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
caretPosition && setSelectionRange(caretPosition);
|
||||||
|
}, [caretPosition]);
|
||||||
|
|
||||||
return <label
|
return <label
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-text-field": true,
|
"vm-text-field": true,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
&_textarea:after {
|
&_textarea:after {
|
||||||
content: attr(data-replicated-value) " ";
|
content: attr(data-replicated-value) " ";
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ export const useFetchQueryOptions = ({ valueByContext, metric, label, context }:
|
||||||
const cachedData = autocompleteCache.get(key);
|
const cachedData = autocompleteCache.get(key);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
setter(processData(cachedData, type));
|
setter(processData(cachedData, type));
|
||||||
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await fetch(`${serverUrl}/api/v1/${urlSuffix}?${params}`, { signal });
|
const response = await fetch(`${serverUrl}/api/v1/${urlSuffix}?${params}`, { signal });
|
||||||
|
@ -104,13 +105,13 @@ export const useFetchQueryOptions = ({ valueByContext, metric, label, context }:
|
||||||
setter(processData(data, type));
|
setter(processData(data, type));
|
||||||
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: data } });
|
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: data } });
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name !== "AbortError") {
|
if (e instanceof Error && e.name !== "AbortError") {
|
||||||
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: [] } });
|
queryDispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: [] } });
|
||||||
|
setLoading(false);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
|
||||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth/): support regex matching when routing incoming requests based on HTTP [query args](https://en.wikipedia.org/wiki/Query_string) via `src_query_args` option at `url_map`. See [these docs](https://docs.victoriametrics.com/vmauth/#generic-http-proxy-for-different-backends) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6070).
|
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth/): support regex matching when routing incoming requests based on HTTP [query args](https://en.wikipedia.org/wiki/Query_string) via `src_query_args` option at `url_map`. See [these docs](https://docs.victoriametrics.com/vmauth/#generic-http-proxy-for-different-backends) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6070).
|
||||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): optimize auto-suggestion performance for metric names when the database contains big number of unique time series.
|
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): optimize auto-suggestion performance for metric names when the database contains big number of unique time series.
|
||||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): in the Select component, user-entered values are now preserved on blur if they match options in the list.
|
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): in the Select component, user-entered values are now preserved on blur if they match options in the list.
|
||||||
|
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): auto-suggestion triggers at any cursor position in the query input. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5864).
|
||||||
|
|
||||||
* BUGFIX: [downsampling](https://docs.victoriametrics.com/#downsampling): skip unnecessary index lookups if downsampling wasn't set for ENT versions of VictoriaMetrics. Before, users of VictoriaMetrics ENT could have experience elevated CPU usage even if no downsampling was configured. The issue was introduced in [v1.100.0](https://docs.victoriametrics.com/changelog/#v11000).
|
* BUGFIX: [downsampling](https://docs.victoriametrics.com/#downsampling): skip unnecessary index lookups if downsampling wasn't set for ENT versions of VictoriaMetrics. Before, users of VictoriaMetrics ENT could have experience elevated CPU usage even if no downsampling was configured. The issue was introduced in [v1.100.0](https://docs.victoriametrics.com/changelog/#v11000).
|
||||||
* BUGFIX: [downsampling](https://docs.victoriametrics.com/#downsampling): properly populate downsampling metadata for data parts created by VictoriaMetrics ENT versions lower than v1.100.0. The bug could trigger the downsampling actions for parts that were downsampled already. This bug doesn't have any negative effect apart from spending extra CPU resources on the repeated downsampling. The issue was introduced in [v1.100.0](https://docs.victoriametrics.com/changelog/#v11000).
|
* BUGFIX: [downsampling](https://docs.victoriametrics.com/#downsampling): properly populate downsampling metadata for data parts created by VictoriaMetrics ENT versions lower than v1.100.0. The bug could trigger the downsampling actions for parts that were downsampled already. This bug doesn't have any negative effect apart from spending extra CPU resources on the repeated downsampling. The issue was introduced in [v1.100.0](https://docs.victoriametrics.com/changelog/#v11000).
|
||||||
|
|
Loading…
Reference in a new issue