logs filter form
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Field, type InjectedFormProps, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
DEBOUNCE_FILTER_TIMEOUT,
|
||||
DEFAULT_LOGS_FILTER,
|
||||
FORM_NAME,
|
||||
RESPONSE_FILTER,
|
||||
RESPONSE_FILTER_QUERIES,
|
||||
} from '../../../helpers/constants';
|
||||
@@ -18,182 +17,115 @@ import useDebounce from '../../../helpers/useDebounce';
|
||||
|
||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import { RootState } from '../../../initialState';
|
||||
import { SearchField } from './SearchField';
|
||||
|
||||
interface renderFilterFieldProps {
|
||||
input: {
|
||||
value: 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;
|
||||
};
|
||||
export type FormValues = {
|
||||
search: string;
|
||||
response_status: string;
|
||||
}
|
||||
|
||||
const renderFilterField = ({
|
||||
input,
|
||||
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 = {
|
||||
type Props = {
|
||||
initialValues: FormValues;
|
||||
className?: string;
|
||||
responseStatusClass?: string;
|
||||
setIsLoading: (...args: unknown[]) => unknown;
|
||||
};
|
||||
|
||||
const Form = (props: FiltersFormProps & InjectedFormProps) => {
|
||||
const { className = '', responseStatusClass, setIsLoading, change } = props;
|
||||
|
||||
export const Form = ({ initialValues, className, setIsLoading }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { response_status, search } = useSelector(
|
||||
(state: RootState) => state?.form[FORM_NAME.LOGS_FILTER].values,
|
||||
shallowEqual,
|
||||
);
|
||||
const {
|
||||
register,
|
||||
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(() => {
|
||||
dispatch(
|
||||
setLogsFilter({
|
||||
response_status,
|
||||
response_status: responseStatusValue,
|
||||
search: debouncedSearch,
|
||||
}),
|
||||
);
|
||||
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
||||
}, [response_status, debouncedSearch]);
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);
|
||||
}, [responseStatusValue, debouncedSearch]);
|
||||
|
||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {
|
||||
setValue('response_status', DEFAULT_LOGS_FILTER.response_status);
|
||||
}
|
||||
}, [responseStatusValue, setValue]);
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setValue('search', DEFAULT_LOGS_FILTER.search);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onEnterPress = (e: any) => {
|
||||
const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
<SearchField
|
||||
value={searchValue}
|
||||
handleChange={(val) => setValue('search', val)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={onEnterPress}
|
||||
onClear={onInputClear}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames(
|
||||
'form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent',
|
||||
responseStatusClass,
|
||||
)}>
|
||||
<select
|
||||
{...register('response_status')}
|
||||
className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block"
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
|
||||
<option key={LABEL} value={QUERY} disabled={disabled}>
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const FiltersForm = reduxForm<Record<string, any>, FiltersFormProps>({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
||||
56
client/src/components/Logs/Filters/SearchField.tsx
Normal file
56
client/src/components/Logs/Filters/SearchField.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,17 +2,17 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { FiltersForm } from './Form';
|
||||
import { Form, FormValues } from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
interface FiltersProps {
|
||||
filter: object;
|
||||
initialValues: FormValues;
|
||||
processingGetLogs: boolean;
|
||||
setIsLoading: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
const Filters = ({ initialValues, setIsLoading }: FiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -38,7 +38,10 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
</svg>
|
||||
</button>
|
||||
</h1>
|
||||
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} />
|
||||
<Form
|
||||
setIsLoading={setIsLoading}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -175,7 +175,7 @@ const Logs = () => {
|
||||
const renderPage = () => (
|
||||
<>
|
||||
<Filters
|
||||
filter={{
|
||||
initialValues={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user