logs filter form

This commit is contained in:
Ildar Kamalov
2025-01-14 18:45:29 +03:00
parent aeab662335
commit ddc1f73554
4 changed files with 125 additions and 134 deletions

View File

@@ -1,15 +1,14 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Field, type InjectedFormProps, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { useForm } from 'react-hook-form';
import { import {
DEBOUNCE_FILTER_TIMEOUT, DEBOUNCE_FILTER_TIMEOUT,
DEFAULT_LOGS_FILTER, DEFAULT_LOGS_FILTER,
FORM_NAME,
RESPONSE_FILTER, RESPONSE_FILTER,
RESPONSE_FILTER_QUERIES, RESPONSE_FILTER_QUERIES,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
@@ -18,182 +17,115 @@ import useDebounce from '../../../helpers/useDebounce';
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers'; import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
import Tooltip from '../../ui/Tooltip'; import { SearchField } from './SearchField';
import { RootState } from '../../../initialState';
interface renderFilterFieldProps { export type FormValues = {
input: { search: string;
value: string; response_status: string;
};
id: string;
onClearInputClick: (...args: unknown[]) => unknown;
className?: string;
placeholder?: string;
type?: string;
disabled?: boolean;
autoComplete?: string;
tooltip?: string;
onKeyDown?: (...args: unknown[]) => unknown;
normalizeOnBlur?: (...args: unknown[]) => unknown;
meta: {
touched?: boolean;
error?: object;
};
} }
const renderFilterField = ({ type Props = {
input, initialValues: FormValues;
id,
className,
placeholder,
type,
disabled,
autoComplete,
tooltip,
meta: { touched, error },
onClearInputClick,
onKeyDown,
normalizeOnBlur,
}: renderFilterFieldProps) => {
const onBlur = (event: any) => createOnBlurHandler(event, input, normalizeOnBlur);
return (
<>
<div className="input-group-search input-group-search__icon--magnifier">
<svg className="icons icon--24 icon--gray">
<use xlinkHref="#magnifier" />
</svg>
</div>
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
aria-label={placeholder}
onKeyDown={onKeyDown}
onBlur={onBlur}
/>
<div
className={classNames('input-group-search input-group-search__icon--cross', {
invisible: input.value.length < 1,
})}>
<svg className="icons icon--20 icon--gray" onClick={onClearInputClick}>
<use xlinkHref="#cross" />
</svg>
</div>
<span className="input-group-search input-group-search__icon--tooltip">
<Tooltip content={tooltip} className="tooltip-container">
<svg className="icons icon--20 icon--gray">
<use xlinkHref="#question" />
</svg>
</Tooltip>
</span>
{!disabled && touched && error && <span className="form__message form__message--error">{error}</span>}
</>
);
};
const FORM_NAMES = {
search: 'search',
response_status: 'response_status',
};
type FiltersFormProps = {
className?: string; className?: string;
responseStatusClass?: string;
setIsLoading: (...args: unknown[]) => unknown; setIsLoading: (...args: unknown[]) => unknown;
}; };
const Form = (props: FiltersFormProps & InjectedFormProps) => { export const Form = ({ initialValues, className, setIsLoading }: Props) => {
const { className = '', responseStatusClass, setIsLoading, change } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const { response_status, search } = useSelector( const {
(state: RootState) => state?.form[FORM_NAME.LOGS_FILTER].values, register,
shallowEqual, watch,
); setValue,
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
search: initialValues.search || DEFAULT_LOGS_FILTER.search,
response_status: initialValues.response_status || DEFAULT_LOGS_FILTER.response_status,
},
});
const [debouncedSearch, setDebouncedSearch] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT); const searchValue = watch('search');
const responseStatusValue = watch('response_status');
const [debouncedSearch, setDebouncedSearch] = useDebounce(
searchValue.trim(),
DEBOUNCE_FILTER_TIMEOUT
);
useEffect(() => { useEffect(() => {
dispatch( dispatch(
setLogsFilter({ setLogsFilter({
response_status, response_status: responseStatusValue,
search: debouncedSearch, search: debouncedSearch,
}), }),
); );
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`); history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);
}, [response_status, debouncedSearch]); }, [responseStatusValue, debouncedSearch]);
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) { useEffect(() => {
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]); if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {
} setValue('response_status', DEFAULT_LOGS_FILTER.response_status);
}
}, [responseStatusValue, setValue]);
const onInputClear = async () => { const onInputClear = async () => {
setIsLoading(true); setIsLoading(true);
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]); setValue('search', DEFAULT_LOGS_FILTER.search);
setIsLoading(false); setIsLoading(false);
}; };
const onEnterPress = (e: any) => { const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
setDebouncedSearch(search); setDebouncedSearch(searchValue);
} }
}; };
const normalizeOnBlur = (data: any) => data.trim(); const handleBlur = (e: React.FocusEvent<HTMLInputElement>) =>
createOnBlurHandler(
e,
{
value: e.target.value,
onChange: (v: string) => setValue('search', v),
},
(data: string) => data.trim()
);
return ( return (
<form <form
className="d-flex flex-wrap form-control--container" className="d-flex flex-wrap form-control--container"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
}}> }}
>
<div className="field__search"> <div className="field__search">
<Field <SearchField
id={FORM_NAMES.search} value={searchValue}
name={FORM_NAMES.search} handleChange={(val) => setValue('search', val)}
component={renderFilterField} onBlur={handleBlur}
type="text" onKeyDown={onEnterPress}
className={classNames('form-control form-control--search form-control--transparent', className)} onClear={onInputClear}
placeholder={t('domain_or_client')} placeholder={t('domain_or_client')}
tooltip={t('query_log_strict_search')} tooltip={t('query_log_strict_search')}
onClearInputClick={onInputClear} className={classNames('form-control form-control--search form-control--transparent', className)}
onKeyDown={onEnterPress}
normalizeOnBlur={normalizeOnBlur}
/> />
</div> </div>
<div className="field__select"> <div className="field__select">
<Field <select
name={FORM_NAMES.response_status} {...register('response_status')}
component="select" className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block"
className={classNames( >
'form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent',
responseStatusClass,
)}>
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => ( {Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
<option key={LABEL} value={QUERY} disabled={disabled}> <option key={LABEL} value={QUERY} disabled={disabled}>
{t(LABEL)} {t(LABEL)}
</option> </option>
))} ))}
</Field> </select>
</div> </div>
</form> </form>
); );
}; };
export const FiltersForm = reduxForm<Record<string, any>, FiltersFormProps>({
form: FORM_NAME.LOGS_FILTER,
enableReinitialize: true,
})(Form);

View File

@@ -0,0 +1,56 @@
import React, { ComponentProps } from 'react';
import Tooltip from '../../ui/Tooltip';
interface Props extends ComponentProps<'input'> {
handleChange: (newValue: string) => void;
onClear: () => void;
tooltip?: string;
}
export const SearchField = ({
handleChange,
onClear,
value,
tooltip,
className,
...rest
}: Props) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(e.target.value);
};
return (
<>
<div className="input-group-search input-group-search__icon--magnifier">
<svg className="icons icon--24 icon--gray">
<use xlinkHref="#magnifier" />
</svg>
</div>
<input
className={className}
value={value}
onChange={handleInputChange}
{...rest}
/>
{typeof value === 'string' && value.length > 0 && (
<div
className="input-group-search input-group-search__icon--cross"
onClick={onClear}
>
<svg className="icons icon--20 icon--gray">
<use xlinkHref="#cross" />
</svg>
</div>
)}
{tooltip && (
<span className="input-group-search input-group-search__icon--tooltip">
<Tooltip content={tooltip} className="tooltip-container">
<svg className="icons icon--20 icon--gray">
<use xlinkHref="#question" />
</svg>
</Tooltip>
</span>
)}
</>
);
};

View File

@@ -2,17 +2,17 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { FiltersForm } from './Form'; import { Form, FormValues } from './Form';
import { refreshFilteredLogs } from '../../../actions/queryLogs'; import { refreshFilteredLogs } from '../../../actions/queryLogs';
import { addSuccessToast } from '../../../actions/toasts'; import { addSuccessToast } from '../../../actions/toasts';
interface FiltersProps { interface FiltersProps {
filter: object; initialValues: FormValues;
processingGetLogs: boolean; processingGetLogs: boolean;
setIsLoading: (...args: unknown[]) => unknown; setIsLoading: (...args: unknown[]) => unknown;
} }
const Filters = ({ filter, setIsLoading }: FiltersProps) => { const Filters = ({ initialValues, setIsLoading }: FiltersProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -38,7 +38,10 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
</svg> </svg>
</button> </button>
</h1> </h1>
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} /> <Form
setIsLoading={setIsLoading}
initialValues={initialValues}
/>
</div> </div>
); );
}; };

View File

@@ -175,7 +175,7 @@ const Logs = () => {
const renderPage = () => ( const renderPage = () => (
<> <>
<Filters <Filters
filter={{ initialValues={{
response_status, response_status,
search, search,
}} }}