vmui: fix input cursor position reset (#6530)

### Describe Your Changes

This PR addresses the issue where the cursor jumps to the end of the
input fields in the modal settings window after each keystroke.

### Before fix:

![ezgif-7-4c69805cea](https://github.com/VictoriaMetrics/VictoriaMetrics/assets/29711459/2e99e833-09e3-4b44-89aa-fc1bd3c4346d)

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

(cherry picked from commit e9b71a2883)
This commit is contained in:
Yury Molodov 2024-06-26 11:14:12 +02:00 committed by hagen1778
parent 25f3e700a6
commit 904ec020ed
No known key found for this signature in database
GPG Key ID: 3BF75F3741CA9640
6 changed files with 108 additions and 123 deletions

View File

@ -1,22 +1,17 @@
import React, { FC, useEffect, useState } from "preact/compat"; import React, { FC, useRef } from "preact/compat";
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator"; import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons"; import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button"; import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal"; import Modal from "../../Main/Modal/Modal";
import "./style.scss"; import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip"; import Tooltip from "../../Main/Tooltip/Tooltip";
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator"; import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
import { Theme } from "../../../types";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { getAppModeEnable } from "../../../utils/app-mode"; import { getAppModeEnable } from "../../../utils/app-mode";
import classNames from "classnames"; import classNames from "classnames";
import Timezones from "./Timezones/Timezones"; import Timezones from "./Timezones/Timezones";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
import ThemeControl from "../ThemeControl/ThemeControl"; import ThemeControl from "../ThemeControl/ThemeControl";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean"; import useBoolean from "../../../hooks/useBoolean";
import { getTenantIdFromUrl } from "../../../utils/tenants";
import { AppType } from "../../../types/appType"; import { AppType } from "../../../types/appType";
const title = "Settings"; const title = "Settings";
@ -24,27 +19,18 @@ const title = "Settings";
const { REACT_APP_TYPE } = process.env; const { REACT_APP_TYPE } = process.env;
const isLogsApp = REACT_APP_TYPE === AppType.logs; const isLogsApp = REACT_APP_TYPE === AppType.logs;
export interface ChildComponentHandle {
handleApply: () => void;
}
const GlobalSettings: FC = () => { const GlobalSettings: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const appModeEnable = getAppModeEnable(); const appModeEnable = getAppModeEnable();
const { serverUrl: stateServerUrl, theme } = useAppState();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const { seriesLimits } = useCustomPanelState();
const dispatch = useAppDispatch(); const serverSettingRef = useRef<ChildComponentHandle>(null);
const timeDispatch = useTimeDispatch(); const limitsSettingRef = useRef<ChildComponentHandle>(null);
const customPanelDispatch = useCustomPanelDispatch(); const timezoneSettingRef = useRef<ChildComponentHandle>(null);
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [limits, setLimits] = useState(seriesLimits);
const [timezone, setTimezone] = useState(stateTimezone);
const setDefaultsValues = () => {
setServerUrl(stateServerUrl);
setLimits(seriesLimits);
setTimezone(stateTimezone);
};
const { const {
value: open, value: open,
@ -52,68 +38,35 @@ const GlobalSettings: FC = () => {
setFalse: handleClose, setFalse: handleClose,
} = useBoolean(false); } = useBoolean(false);
const handleCloseAndReset = () => { const handleApply = () => {
handleClose(); serverSettingRef.current && serverSettingRef.current.handleApply();
setDefaultsValues(); limitsSettingRef.current && limitsSettingRef.current.handleApply();
}; timezoneSettingRef.current && timezoneSettingRef.current.handleApply();
const handleChangeTheme = (value: Theme) => {
dispatch({ type: "SET_THEME", payload: value });
};
const handlerApply = () => {
const tenantIdFromUrl = getTenantIdFromUrl(serverUrl);
if (tenantIdFromUrl !== "") {
dispatch({ type: "SET_TENANT_ID", payload: tenantIdFromUrl });
}
dispatch({ type: "SET_SERVER", payload: serverUrl });
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
handleClose(); handleClose();
}; };
useEffect(() => {
// the tenant selector can change the serverUrl
if (stateServerUrl === serverUrl) return;
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
const controls = [ const controls = [
{ {
show: !appModeEnable && !isLogsApp, show: !appModeEnable && !isLogsApp,
component: <ServerConfigurator component: <ServerConfigurator
stateServerUrl={stateServerUrl} ref={serverSettingRef}
serverUrl={serverUrl} onClose={handleClose}
onChange={setServerUrl}
onEnter={handlerApply}
/> />
}, },
{ {
show: !isLogsApp, show: !isLogsApp,
component: <LimitsConfigurator component: <LimitsConfigurator
limits={limits} ref={limitsSettingRef}
onChange={setLimits} onClose={handleClose}
onEnter={handlerApply}
/> />
}, },
{ {
show: true, show: true,
component: <Timezones component: <Timezones ref={timezoneSettingRef}/>
timezoneState={timezone}
defaultTimezone={defaultTimezone}
onChange={setTimezone}
/>
}, },
{ {
show: !appModeEnable, show: !appModeEnable,
component: <ThemeControl component: <ThemeControl/>
theme={theme}
onChange={handleChangeTheme}
/>
} }
].filter(control => control.show); ].filter(control => control.show);
@ -146,7 +99,7 @@ const GlobalSettings: FC = () => {
{open && ( {open && (
<Modal <Modal
title={title} title={title}
onClose={handleCloseAndReset} onClose={handleClose}
> >
<div <div
className={classNames({ className={classNames({
@ -166,14 +119,14 @@ const GlobalSettings: FC = () => {
<Button <Button
color="error" color="error"
variant="outlined" variant="outlined"
onClick={handleCloseAndReset} onClick={handleClose}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
color="primary" color="primary"
variant="contained" variant="contained"
onClick={handlerApply} onClick={handleApply}
> >
Apply Apply
</Button> </Button>

View File

@ -1,5 +1,5 @@
import React, { FC, useState } from "preact/compat"; import React, { forwardRef, useCallback, useImperativeHandle, useState } from "preact/compat";
import { DisplayType, ErrorTypes, SeriesLimits } from "../../../../types"; import { DisplayType, ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField"; import TextField from "../../../Main/TextField/TextField";
import Tooltip from "../../../Main/Tooltip/Tooltip"; import Tooltip from "../../../Main/Tooltip/Tooltip";
import { InfoIcon, RestartIcon } from "../../../Main/Icons"; import { InfoIcon, RestartIcon } from "../../../Main/Icons";
@ -8,11 +8,11 @@ import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
import "./style.scss"; import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import { ChildComponentHandle } from "../GlobalSettings";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
export interface ServerConfiguratorProps { interface ServerConfiguratorProps {
limits: SeriesLimits onClose: () => void
onChange: (limits: SeriesLimits) => void
onEnter: () => void
} }
const fields: {label: string, type: DisplayType}[] = [ const fields: {label: string, type: DisplayType}[] = [
@ -21,31 +21,38 @@ const fields: {label: string, type: DisplayType}[] = [
{ label: "Table", type: DisplayType.table } { label: "Table", type: DisplayType.table }
]; ];
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => { const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorProps>(({ onClose }, ref) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { seriesLimits } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const [limits, setLimits] = useState(seriesLimits);
const [error, setError] = useState({ const [error, setError] = useState({
table: "", table: "",
chart: "", chart: "",
code: "" code: ""
}); });
const handleChange = (val: string, type: DisplayType) => { const handleReset = () => {
setLimits(DEFAULT_MAX_SERIES);
};
const createChangeHandler = (type: DisplayType) => (val: string) => {
const value = val || ""; const value = val || "";
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" })); setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
onChange({ setLimits({
...limits, ...limits,
[type]: !value ? Infinity : value [type]: !value ? Infinity : value
}); });
}; };
const handleReset = () => { const handleApply = useCallback(() => {
onChange(DEFAULT_MAX_SERIES); customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
}; onClose();
}, [limits]);
const createChangeHandler = (type: DisplayType) => (val: string) => { useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
handleChange(val, type);
};
return ( return (
<div className="vm-limits-configurator"> <div className="vm-limits-configurator">
@ -84,7 +91,7 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
value={limits[f.type]} value={limits[f.type]}
error={error[f.type]} error={error[f.type]}
onChange={createChangeHandler(f.type)} onChange={createChangeHandler(f.type)}
onEnter={onEnter} onEnter={handleApply}
type="number" type="number"
/> />
</div> </div>
@ -92,6 +99,6 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
</div> </div>
</div> </div>
); );
}; });
export default LimitsConfigurator; export default LimitsConfigurator;

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useState } from "preact/compat"; import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "preact/compat";
import { ErrorTypes } from "../../../../types"; import { ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField"; import TextField from "../../../Main/TextField/TextField";
import { isValidHttpUrl } from "../../../../utils/url"; import { isValidHttpUrl } from "../../../../utils/url";
@ -7,12 +7,12 @@ import { StorageIcon } from "../../../Main/Icons";
import Tooltip from "../../../Main/Tooltip/Tooltip"; import Tooltip from "../../../Main/Tooltip/Tooltip";
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../utils/storage"; import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../utils/storage";
import useBoolean from "../../../../hooks/useBoolean"; import useBoolean from "../../../../hooks/useBoolean";
import { ChildComponentHandle } from "../GlobalSettings";
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
import { getTenantIdFromUrl } from "../../../../utils/tenants";
export interface ServerConfiguratorProps { interface ServerConfiguratorProps {
serverUrl: string onClose: () => void;
stateServerUrl: string
onChange: (url: string) => void
onEnter: () => void
} }
const tooltipSave = { const tooltipSave = {
@ -20,24 +20,33 @@ const tooltipSave = {
disable: "Disable to stop saving the server URL to local storage, reverting to the default URL on page refresh." disable: "Disable to stop saving the server URL to local storage, reverting to the default URL on page refresh."
}; };
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ const ServerConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorProps>(({ onClose }, ref) => {
serverUrl, const { serverUrl: stateServerUrl } = useAppState();
stateServerUrl, const dispatch = useAppDispatch();
onChange ,
onEnter
}) => {
const { const {
value: enabledStorage, value: enabledStorage,
toggle: handleToggleStorage, toggle: handleToggleStorage,
} = useBoolean(!!getFromStorage("SERVER_URL")); } = useBoolean(!!getFromStorage("SERVER_URL"));
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [error, setError] = useState(""); const [error, setError] = useState("");
const onChangeServer = (val: string) => { const handleChange = (val: string) => {
const value = val || ""; const value = val || "";
onChange(value); setServerUrl(value);
setError(""); setError("");
}; };
const handleApply = useCallback(() => {
const tenantIdFromUrl = getTenantIdFromUrl(serverUrl);
if (tenantIdFromUrl !== "") {
dispatch({ type: "SET_TENANT_ID", payload: tenantIdFromUrl });
}
dispatch({ type: "SET_SERVER", payload: serverUrl });
onClose();
}, [serverUrl]);
useEffect(() => { useEffect(() => {
if (!stateServerUrl) setError(ErrorTypes.emptyServer); if (!stateServerUrl) setError(ErrorTypes.emptyServer);
if (!isValidHttpUrl(stateServerUrl)) setError(ErrorTypes.validServer); if (!isValidHttpUrl(stateServerUrl)) setError(ErrorTypes.validServer);
@ -57,6 +66,14 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
} }
}, [serverUrl]); }, [serverUrl]);
useEffect(() => {
// the tenant selector can change the serverUrl
if (stateServerUrl === serverUrl) return;
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
return ( return (
<div> <div>
<div className="vm-server-configurator__title"> <div className="vm-server-configurator__title">
@ -67,8 +84,8 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
autofocus autofocus
value={serverUrl} value={serverUrl}
error={error} error={error}
onChange={onChangeServer} onChange={handleChange}
onEnter={onEnter} onEnter={handleApply}
inputmode="url" inputmode="url"
/> />
<Tooltip title={enabledStorage ? tooltipSave.disable : tooltipSave.enable}> <Tooltip title={enabledStorage ? tooltipSave.disable : tooltipSave.enable}>
@ -83,6 +100,6 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
</div> </div>
</div> </div>
); );
}; });
export default ServerConfigurator; export default ServerConfigurator;

View File

@ -1,4 +1,4 @@
import React, { FC, useMemo, useRef, useState } from "preact/compat"; import React, { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/compat";
import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time"; import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
import { ArrowDropDownIcon } from "../../../Main/Icons"; import { ArrowDropDownIcon } from "../../../Main/Icons";
import classNames from "classnames"; import classNames from "classnames";
@ -10,12 +10,7 @@ import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean"; import useBoolean from "../../../../hooks/useBoolean";
import WarningTimezone from "./WarningTimezone"; import WarningTimezone from "./WarningTimezone";
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
interface TimezonesProps {
timezoneState: string;
defaultTimezone?: string;
onChange: (val: string) => void;
}
interface PinnedTimezone extends Timezone { interface PinnedTimezone extends Timezone {
title: string; title: string;
@ -24,10 +19,14 @@ interface PinnedTimezone extends Timezone {
const browserTimezone = getBrowserTimezone(); const browserTimezone = getBrowserTimezone();
const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChange }) => { const Timezones: FC = forwardRef((props, ref) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList(); const timezones = getTimezoneList();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const timeDispatch = useTimeDispatch();
const [timezone, setTimezone] = useState(stateTimezone);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -68,16 +67,16 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]); const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
const activeTimezone = useMemo(() => ({ const activeTimezone = useMemo(() => ({
region: timezoneState, region: timezone,
utc: getUTCByTimezone(timezoneState) utc: getUTCByTimezone(timezone)
}), [timezoneState]); }), [timezone]);
const handleChangeSearch = (val: string) => { const handleChangeSearch = (val: string) => {
setSearch(val); setSearch(val);
}; };
const handleSetTimezone = (val: Timezone) => { const handleSetTimezone = (val: Timezone) => {
onChange(val.region); setTimezone(val.region);
setSearch(""); setSearch("");
handleCloseList(); handleCloseList();
}; };
@ -86,6 +85,16 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
handleSetTimezone(val); handleSetTimezone(val);
}; };
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
useImperativeHandle(ref, () => ({
handleApply: () => {
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
}
}), [timezone]);
return ( return (
<div className="vm-timezones"> <div className="vm-timezones">
<div className="vm-server-configurator__title"> <div className="vm-server-configurator__title">
@ -169,6 +178,6 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
</Popper> </Popper>
</div> </div>
); );
}; });
export default Timezones; export default Timezones;

View File

@ -5,18 +5,17 @@ import Toggle from "../../Main/Toggle/Toggle";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames"; import classNames from "classnames";
import { FC } from "preact/compat"; import { FC } from "preact/compat";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
interface ThemeControlProps {
theme: Theme;
onChange: (val: Theme) => void
}
const options = Object.values(Theme).map(value => ({ title: value, value })); const options = Object.values(Theme).map(value => ({ title: value, value }));
const ThemeControl: FC<ThemeControlProps> = ({ theme, onChange }) => { const ThemeControl: FC = () => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const dispatch = useAppDispatch();
const { theme } = useAppState();
const handleClickItem = (value: string) => { const handleClickItem = (value: string) => {
onChange(value as Theme); dispatch({ type: "SET_THEME", payload: value as Theme });
}; };
return ( return (

View File

@ -32,7 +32,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
* FEATURE: [dashboards](https://grafana.com/orgs/victoriametrics): add [Grafana dashboard](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/dashboards/vmauth.json) and [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmauth.yml) for [vmauth](https://docs.victoriametrics.com/vmauth/) dashboard. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4313) for details. * FEATURE: [dashboards](https://grafana.com/orgs/victoriametrics): add [Grafana dashboard](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/dashboards/vmauth.json) and [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmauth.yml) for [vmauth](https://docs.victoriametrics.com/vmauth/) dashboard. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4313) for details.
* BUGFIX: [docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#docker-compose-environment-for-victoriametrics): fix incorrect link to vmui from [VictoriaMetrics plugin in Grafana](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#grafana). * BUGFIX: [docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#docker-compose-environment-for-victoriametrics): fix incorrect link to vmui from [VictoriaMetrics plugin in Grafana](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#grafana).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix input cursor position reset in modal settings. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6530).
## [v1.102.0-rc2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.0-rc2) ## [v1.102.0-rc2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.0-rc2)