logs filter form
This commit is contained in:
@@ -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);
|
|
||||||
|
|||||||
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const Logs = () => {
|
|||||||
const renderPage = () => (
|
const renderPage = () => (
|
||||||
<>
|
<>
|
||||||
<Filters
|
<Filters
|
||||||
filter={{
|
initialValues={{
|
||||||
response_status,
|
response_status,
|
||||||
search,
|
search,
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user