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/).
This commit is contained in:
Yury Molodov 2024-06-26 11:14:12 +02:00 committed by GitHub
parent 6cab811134
commit e9b71a2883
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
import { Theme } from "../../../types";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { getAppModeEnable } from "../../../utils/app-mode";
import classNames from "classnames";
import Timezones from "./Timezones/Timezones";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
import ThemeControl from "../ThemeControl/ThemeControl";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
import { getTenantIdFromUrl } from "../../../utils/tenants";
import { AppType } from "../../../types/appType";
const title = "Settings";
@ -24,27 +19,18 @@ const title = "Settings";
const { REACT_APP_TYPE } = process.env;
const isLogsApp = REACT_APP_TYPE === AppType.logs;
export interface ChildComponentHandle {
handleApply: () => void;
}
const GlobalSettings: FC = () => {
const { isMobile } = useDeviceDetect();
const appModeEnable = getAppModeEnable();
const { serverUrl: stateServerUrl, theme } = useAppState();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const { seriesLimits } = useCustomPanelState();
const dispatch = useAppDispatch();
const timeDispatch = useTimeDispatch();
const customPanelDispatch = useCustomPanelDispatch();
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [limits, setLimits] = useState(seriesLimits);
const [timezone, setTimezone] = useState(stateTimezone);
const setDefaultsValues = () => {
setServerUrl(stateServerUrl);
setLimits(seriesLimits);
setTimezone(stateTimezone);
};
const serverSettingRef = useRef<ChildComponentHandle>(null);
const limitsSettingRef = useRef<ChildComponentHandle>(null);
const timezoneSettingRef = useRef<ChildComponentHandle>(null);
const {
value: open,
@ -52,68 +38,35 @@ const GlobalSettings: FC = () => {
setFalse: handleClose,
} = useBoolean(false);
const handleCloseAndReset = () => {
handleClose();
setDefaultsValues();
};
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 });
const handleApply = () => {
serverSettingRef.current && serverSettingRef.current.handleApply();
limitsSettingRef.current && limitsSettingRef.current.handleApply();
timezoneSettingRef.current && timezoneSettingRef.current.handleApply();
handleClose();
};
useEffect(() => {
// the tenant selector can change the serverUrl
if (stateServerUrl === serverUrl) return;
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
const controls = [
{
show: !appModeEnable && !isLogsApp,
component: <ServerConfigurator
stateServerUrl={stateServerUrl}
serverUrl={serverUrl}
onChange={setServerUrl}
onEnter={handlerApply}
ref={serverSettingRef}
onClose={handleClose}
/>
},
{
show: !isLogsApp,
component: <LimitsConfigurator
limits={limits}
onChange={setLimits}
onEnter={handlerApply}
ref={limitsSettingRef}
onClose={handleClose}
/>
},
{
show: true,
component: <Timezones
timezoneState={timezone}
defaultTimezone={defaultTimezone}
onChange={setTimezone}
/>
component: <Timezones ref={timezoneSettingRef}/>
},
{
show: !appModeEnable,
component: <ThemeControl
theme={theme}
onChange={handleChangeTheme}
/>
component: <ThemeControl/>
}
].filter(control => control.show);
@ -146,7 +99,7 @@ const GlobalSettings: FC = () => {
{open && (
<Modal
title={title}
onClose={handleCloseAndReset}
onClose={handleClose}
>
<div
className={classNames({
@ -166,14 +119,14 @@ const GlobalSettings: FC = () => {
<Button
color="error"
variant="outlined"
onClick={handleCloseAndReset}
onClick={handleClose}
>
Cancel
</Button>
<Button
color="primary"
variant="contained"
onClick={handlerApply}
onClick={handleApply}
>
Apply
</Button>

View File

@ -1,5 +1,5 @@
import React, { FC, useState } from "preact/compat";
import { DisplayType, ErrorTypes, SeriesLimits } from "../../../../types";
import React, { forwardRef, useCallback, useImperativeHandle, useState } from "preact/compat";
import { DisplayType, ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { InfoIcon, RestartIcon } from "../../../Main/Icons";
@ -8,11 +8,11 @@ import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import { ChildComponentHandle } from "../GlobalSettings";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
export interface ServerConfiguratorProps {
limits: SeriesLimits
onChange: (limits: SeriesLimits) => void
onEnter: () => void
interface ServerConfiguratorProps {
onClose: () => void
}
const fields: {label: string, type: DisplayType}[] = [
@ -21,31 +21,38 @@ const fields: {label: string, type: DisplayType}[] = [
{ label: "Table", type: DisplayType.table }
];
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorProps>(({ onClose }, ref) => {
const { isMobile } = useDeviceDetect();
const { seriesLimits } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const [limits, setLimits] = useState(seriesLimits);
const [error, setError] = useState({
table: "",
chart: "",
code: ""
});
const handleChange = (val: string, type: DisplayType) => {
const handleReset = () => {
setLimits(DEFAULT_MAX_SERIES);
};
const createChangeHandler = (type: DisplayType) => (val: string) => {
const value = val || "";
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
onChange({
setLimits({
...limits,
[type]: !value ? Infinity : value
});
};
const handleReset = () => {
onChange(DEFAULT_MAX_SERIES);
};
const handleApply = useCallback(() => {
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
onClose();
}, [limits]);
const createChangeHandler = (type: DisplayType) => (val: string) => {
handleChange(val, type);
};
useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
return (
<div className="vm-limits-configurator">
@ -84,7 +91,7 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
value={limits[f.type]}
error={error[f.type]}
onChange={createChangeHandler(f.type)}
onEnter={onEnter}
onEnter={handleApply}
type="number"
/>
</div>
@ -92,6 +99,6 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
</div>
</div>
);
};
});
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 TextField from "../../../Main/TextField/TextField";
import { isValidHttpUrl } from "../../../../utils/url";
@ -7,12 +7,12 @@ import { StorageIcon } from "../../../Main/Icons";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../utils/storage";
import useBoolean from "../../../../hooks/useBoolean";
import { ChildComponentHandle } from "../GlobalSettings";
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
import { getTenantIdFromUrl } from "../../../../utils/tenants";
export interface ServerConfiguratorProps {
serverUrl: string
stateServerUrl: string
onChange: (url: string) => void
onEnter: () => void
interface ServerConfiguratorProps {
onClose: () => void;
}
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."
};
const ServerConfigurator: FC<ServerConfiguratorProps> = ({
serverUrl,
stateServerUrl,
onChange ,
onEnter
}) => {
const ServerConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorProps>(({ onClose }, ref) => {
const { serverUrl: stateServerUrl } = useAppState();
const dispatch = useAppDispatch();
const {
value: enabledStorage,
toggle: handleToggleStorage,
} = useBoolean(!!getFromStorage("SERVER_URL"));
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [error, setError] = useState("");
const onChangeServer = (val: string) => {
const handleChange = (val: string) => {
const value = val || "";
onChange(value);
setServerUrl(value);
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(() => {
if (!stateServerUrl) setError(ErrorTypes.emptyServer);
if (!isValidHttpUrl(stateServerUrl)) setError(ErrorTypes.validServer);
@ -57,6 +66,14 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
}
}, [serverUrl]);
useEffect(() => {
// the tenant selector can change the serverUrl
if (stateServerUrl === serverUrl) return;
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
return (
<div>
<div className="vm-server-configurator__title">
@ -67,8 +84,8 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
autofocus
value={serverUrl}
error={error}
onChange={onChangeServer}
onEnter={onEnter}
onChange={handleChange}
onEnter={handleApply}
inputmode="url"
/>
<Tooltip title={enabledStorage ? tooltipSave.disable : tooltipSave.enable}>
@ -83,6 +100,6 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
</div>
</div>
);
};
});
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 { ArrowDropDownIcon } from "../../../Main/Icons";
import classNames from "classnames";
@ -10,12 +10,7 @@ import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
import WarningTimezone from "./WarningTimezone";
interface TimezonesProps {
timezoneState: string;
defaultTimezone?: string;
onChange: (val: string) => void;
}
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
interface PinnedTimezone extends Timezone {
title: string;
@ -24,10 +19,14 @@ interface PinnedTimezone extends Timezone {
const browserTimezone = getBrowserTimezone();
const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChange }) => {
const Timezones: FC = forwardRef((props, ref) => {
const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const timeDispatch = useTimeDispatch();
const [timezone, setTimezone] = useState(stateTimezone);
const [search, setSearch] = useState("");
const targetRef = useRef<HTMLDivElement>(null);
@ -68,16 +67,16 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
const activeTimezone = useMemo(() => ({
region: timezoneState,
utc: getUTCByTimezone(timezoneState)
}), [timezoneState]);
region: timezone,
utc: getUTCByTimezone(timezone)
}), [timezone]);
const handleChangeSearch = (val: string) => {
setSearch(val);
};
const handleSetTimezone = (val: Timezone) => {
onChange(val.region);
setTimezone(val.region);
setSearch("");
handleCloseList();
};
@ -86,6 +85,16 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
handleSetTimezone(val);
};
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
useImperativeHandle(ref, () => ({
handleApply: () => {
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
}
}), [timezone]);
return (
<div className="vm-timezones">
<div className="vm-server-configurator__title">
@ -169,6 +178,6 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChang
</Popper>
</div>
);
};
});
export default Timezones;

View File

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