+ client: separate filters from the table component

This commit is contained in:
Ildar Kamalov 2019-11-15 10:51:45 +03:00 committed by Simon Zolin
parent 6b64d393bd
commit e243e69a6e
8 changed files with 263 additions and 94 deletions

View File

@ -4,8 +4,19 @@ import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index'; import { addErrorToast, addSuccessToast } from './index';
import { normalizeLogs } from '../helpers/helpers'; import { normalizeLogs } from '../helpers/helpers';
const getLogsWithParams = async (config) => {
const { older_than, filter, ...values } = config;
const rawLogs = await apiClient.getQueryLog({ ...filter, older_than });
const { data, oldest } = rawLogs;
const logs = normalizeLogs(data);
return {
logs, oldest, older_than, filter, ...values,
};
};
export const setLogsPagination = createAction('LOGS_PAGINATION'); export const setLogsPagination = createAction('LOGS_PAGINATION');
export const setLogsFilter = createAction('LOGS_FILTER'); export const setLogsPage = createAction('SET_LOG_PAGE');
export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsFailure = createAction('GET_LOGS_FAILURE');
@ -14,19 +25,30 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = config => async (dispatch) => { export const getLogs = config => async (dispatch) => {
dispatch(getLogsRequest()); dispatch(getLogsRequest());
try { try {
const { filter, older_than } = config; const logs = await getLogsWithParams(config);
const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); dispatch(getLogsSuccess(logs));
const { data, oldest } = rawLogs;
const logs = normalizeLogs(data);
dispatch(getLogsSuccess({
logs, oldest, filter, ...config,
}));
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error)); dispatch(getLogsFailure(error));
} }
}; };
export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
export const setLogsFilterFailure = createAction('SET_LOGS_FILTER_FAILURE');
export const setLogsFilterSuccess = createAction('SET_LOGS_FILTER_SUCCESS');
export const setLogsFilter = filter => async (dispatch) => {
dispatch(setLogsFilterRequest());
try {
const logs = await getLogsWithParams({ older_than: '', filter });
dispatch(setLogsFilterSuccess(logs));
dispatch(setLogsPage(0));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setLogsFilterFailure(error));
}
};
export const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST'); export const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE'); export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS'); export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');

View File

@ -0,0 +1,116 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import { renderField } from '../../../helpers/form';
import { RESPONSE_FILTER } from '../../../helpers/constants';
import Tooltip from '../../ui/Tooltip';
const renderFilterField = ({
input,
id,
className,
placeholder,
type,
disabled,
autoComplete,
tooltip,
meta: { touched, error },
}) => (
<Fragment>
<div className="logs__input-wrap">
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
/>
<span className="logs__notice">
<Tooltip text={tooltip} type='tooltip-custom--logs' />
</span>
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</div>
</Fragment>
);
const Form = (props) => {
const {
t,
handleChange,
} = props;
return (
<form onSubmit={handleChange}>
<div className="row">
<div className="col-3">
<Field
id="domain"
name="domain"
component={renderFilterField}
type="text"
className="form-control"
placeholder={t('domain_name_table_header')}
tooltip={t('query_log_strict_search')}
onChange={handleChange}
/>
</div>
<div className="col-3">
<Field
id="type"
name="type"
component={renderField}
type="text"
className="form-control"
placeholder={t('type_table_header')}
onChange={handleChange}
/>
</div>
<div className="col-3">
<Field
name="response"
component="select"
className="form-control custom-select"
>
<option value={RESPONSE_FILTER.ALL}>
<Trans>show_all_filter_type</Trans>
</option>
<option value={RESPONSE_FILTER.FILTERED}>
<Trans>show_filtered_type</Trans>
</option>
</Field>
</div>
<div className="col-3">
<Field
id="client"
name="client"
component={renderFilterField}
type="text"
className="form-control"
placeholder={t('client_table_header')}
tooltip={t('query_log_strict_search')}
onChange={handleChange}
/>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleChange: PropTypes.func,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({
form: 'logsFilterForm',
}),
])(Form);

View File

@ -0,0 +1,45 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import { DEBOUNCE_FILTER_TIMEOUT, RESPONSE_FILTER } from '../../../helpers/constants';
import { isValidQuestionType } from '../../../helpers/helpers';
import Form from './Form';
class Filters extends Component {
getFilters = (filtered) => {
const {
domain, client, type, response,
} = filtered;
return {
filter_domain: domain || '',
filter_client: client || '',
filter_question_type: isValidQuestionType(type) ? type.toUpperCase() : '',
filter_response_status: response === RESPONSE_FILTER.FILTERED ? response : '',
};
};
handleFormChange = debounce((values) => {
const filter = this.getFilters(values);
this.props.setLogsFilter(filter);
}, DEBOUNCE_FILTER_TIMEOUT);
render() {
const { filter } = this.props;
return (
<Form
initialValues={filter}
onChange={this.handleFormChange}
/>
);
}
}
Filters.propTypes = {
filter: PropTypes.object.isRequired,
setLogsFilter: PropTypes.func.isRequired,
};
export default Filters;

View File

@ -5,46 +5,40 @@ import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith'; import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
import debounce from 'lodash/debounce';
import { import {
formatTime, formatTime,
formatDateTime, formatDateTime,
isValidQuestionType,
} from '../../helpers/helpers'; } from '../../helpers/helpers';
import { SERVICES, FILTERED_STATUS, DEBOUNCE_TIMEOUT, DEFAULT_LOGS_FILTER } from '../../helpers/constants'; import { SERVICES, FILTERED_STATUS, DEFAULT_LOGS_FILTER, RESPONSE_FILTER, TABLE_DEFAULT_PAGE_SIZE } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers'; import { getTrackerData } from '../../helpers/trackers/trackers';
import { formatClientCell } from '../../helpers/formatClientCell'; import { formatClientCell } from '../../helpers/formatClientCell';
import Filters from './Filters';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import PopoverFiltered from '../ui/PopoverFilter'; import PopoverFiltered from '../ui/PopoverFilter';
import Popover from '../ui/Popover'; import Popover from '../ui/Popover';
import Tooltip from '../ui/Tooltip';
import './Logs.css'; import './Logs.css';
const TABLE_FIRST_PAGE = 0; const TABLE_FIRST_PAGE = 0;
const TABLE_DEFAULT_PAGE_SIZE = 100;
const INITIAL_REQUEST_DATA = ['', DEFAULT_LOGS_FILTER, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE]; const INITIAL_REQUEST_DATA = ['', DEFAULT_LOGS_FILTER, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE];
const FILTERED_REASON = 'Filtered'; const FILTERED_REASON = 'Filtered';
const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};
class Logs extends Component { class Logs extends Component {
componentDidMount() { componentDidMount() {
this.props.setLogsPage(TABLE_FIRST_PAGE);
this.getLogs(...INITIAL_REQUEST_DATA); this.getLogs(...INITIAL_REQUEST_DATA);
this.props.getFilteringStatus(); this.props.getFilteringStatus();
this.props.getClients(); this.props.getClients();
this.props.getLogsConfig(); this.props.getLogsConfig();
} }
getLogs = (older_than, filter, page, pageSize, filtered) => { getLogs = (older_than, filter, page) => {
if (this.props.queryLogs.enabled) { if (this.props.queryLogs.enabled) {
this.props.getLogs({ this.props.getLogs({
older_than, filter, page, pageSize, filtered, older_than, filter, page, pageSize: TABLE_DEFAULT_PAGE_SIZE,
}); });
} }
}; };
@ -53,16 +47,6 @@ class Logs extends Component {
window.location.reload(); window.location.reload();
}; };
handleLogsFiltering = debounce((older_than, filter, page, pageSize, filtered) => {
this.props.getLogs({
older_than,
filter,
page,
pageSize,
filtered,
});
}, DEBOUNCE_TIMEOUT);
renderTooltip = (isFiltered, rule, filter, service) => renderTooltip = (isFiltered, rule, filter, service) =>
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />; isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
@ -232,70 +216,25 @@ class Logs extends Component {
); );
}; };
getFilterInput = ({ filter, onChange }) => (
<Fragment>
<div className="logs__input-wrap">
<input
type="text"
className="form-control"
onChange={event => onChange(event.target.value)}
value={filter ? filter.value : ''}
/>
<span className="logs__notice">
<Tooltip text={this.props.t('query_log_strict_search')} type='tooltip-custom--logs' />
</span>
</div>
</Fragment>
);
getFilters = (filtered) => {
const filteredObj = filtered.reduce((acc, cur) => ({ ...acc, [cur.id]: cur.value }), {});
const {
domain, client, type, response,
} = filteredObj;
return {
filter_domain: domain || '',
filter_client: client || '',
filter_question_type: isValidQuestionType(type) ? type.toUpperCase() : '',
filter_response_status: response === RESPONSE_FILTER.FILTERED ? response : '',
};
};
fetchData = (state) => { fetchData = (state) => {
const { pageSize, page, pages } = state; const { pages } = state;
const { filter, oldest } = this.props.queryLogs; const {
filter, oldest, page,
} = this.props.queryLogs;
const isLastPage = pages && (page + 1 === pages); const isLastPage = pages && (page + 1 === pages);
if (isLastPage) { if (isLastPage) {
this.getLogs(oldest, filter, page, pageSize, false); this.getLogs(oldest, filter, page);
} else { } else {
this.props.setLogsPagination({ page, pageSize }); this.props.setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE });
} }
}; };
handleFilterChange = (filtered) => {
const filters = this.getFilters(filtered);
this.props.setLogsFilter(filters);
this.handleLogsFiltering('', filters, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE, true);
}
showTotalPagesCount = (pages) => {
const { total, isEntireLog } = this.props.queryLogs;
const showEllipsis = !isEntireLog && total >= 500;
return (
<span className="-totalPages">
{pages || 1}{showEllipsis && '…' }
</span>
);
}
renderLogs() { renderLogs() {
const { queryLogs, dashboard, t } = this.props; const { queryLogs, dashboard, t } = this.props;
const { processingClients } = dashboard; const { processingClients } = dashboard;
const { const {
processingGetLogs, processingGetConfig, logs, pages, processingGetLogs, processingGetConfig, logs, pages, page,
} = queryLogs; } = queryLogs;
const isLoading = processingGetLogs || processingClients || processingGetConfig; const isLoading = processingGetLogs || processingClients || processingGetConfig;
@ -304,7 +243,6 @@ class Logs extends Component {
Header: t('time_table_header'), Header: t('time_table_header'),
accessor: 'time', accessor: 'time',
maxWidth: 100, maxWidth: 100,
filterable: false,
Cell: this.getTimeCell, Cell: this.getTimeCell,
}, },
{ {
@ -360,18 +298,20 @@ class Logs extends Component {
return ( return (
<ReactTable <ReactTable
manual manual
filterable
minRows={5} minRows={5}
page={page}
pages={pages} pages={pages}
columns={columns} columns={columns}
filterable={false}
sortable={false} sortable={false}
data={logs || []} data={logs || []}
loading={isLoading} loading={isLoading}
showPagination={true} showPagination={true}
showPaginationTop={true}
showPageJump={false} showPageJump={false}
showPageSizeOptions={false} showPageSizeOptions={false}
onFetchData={this.fetchData} onFetchData={this.fetchData}
onFilteredChange={this.handleFilterChange} onPageChange={newPage => this.props.setLogsPage(newPage)}
className="logs__table" className="logs__table"
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE} defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
previousText={t('previous_btn')} previousText={t('previous_btn')}
@ -381,7 +321,6 @@ class Logs extends Component {
noDataText={t('no_logs_found')} noDataText={t('no_logs_found')}
pageText={''} pageText={''}
ofText={''} ofText={''}
renderCurrentPage={() => false}
renderTotalPagesCount={() => false} renderTotalPagesCount={() => false}
defaultFilterMethod={(filter, row) => { defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id; const id = filter.pivotId || filter.id;
@ -446,7 +385,17 @@ class Logs extends Component {
<Fragment> <Fragment>
<PageTitle title={t('query_log')}>{refreshButton}</PageTitle> <PageTitle title={t('query_log')}>{refreshButton}</PageTitle>
{enabled && processingGetConfig && <Loading />} {enabled && processingGetConfig && <Loading />}
{enabled && !processingGetConfig && <Card>{this.renderLogs()}</Card>} {enabled && !processingGetConfig && (
<Fragment>
<Card>
<Filters
filter={queryLogs.filter}
setLogsFilter={this.props.setLogsFilter}
/>
</Card>
<Card>{this.renderLogs()}</Card>
</Fragment>
)}
{!enabled && !processingGetConfig && ( {!enabled && !processingGetConfig && (
<Card> <Card>
<div className="lead text-center py-6"> <div className="lead text-center py-6">
@ -479,6 +428,7 @@ Logs.propTypes = {
getLogsConfig: PropTypes.func.isRequired, getLogsConfig: PropTypes.func.isRequired,
setLogsPagination: PropTypes.func.isRequired, setLogsPagination: PropTypes.func.isRequired,
setLogsFilter: PropTypes.func.isRequired, setLogsFilter: PropTypes.func.isRequired,
setLogsPage: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };

View File

@ -64,6 +64,7 @@
top: calc(100% + 10px); top: calc(100% + 10px);
right: -10px; right: -10px;
left: initial; left: initial;
width: 255px;
transform: none; transform: none;
} }

View File

@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { addSuccessToast, getClients } from '../actions'; import { addSuccessToast, getClients } from '../actions';
import { getFilteringStatus, setRules } from '../actions/filtering'; import { getFilteringStatus, setRules } from '../actions/filtering';
import { getLogs, getLogsConfig, setLogsPagination, setLogsFilter } from '../actions/queryLogs'; import { getLogs, getLogsConfig, setLogsPagination, setLogsFilter, setLogsPage } from '../actions/queryLogs';
import Logs from '../components/Logs'; import Logs from '../components/Logs';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -19,6 +19,7 @@ const mapDispatchToProps = {
getLogsConfig, getLogsConfig,
setLogsPagination, setLogsPagination,
setLogsFilter, setLogsFilter,
setLogsPage,
}; };
export default connect( export default connect(

View File

@ -141,6 +141,7 @@ export const STANDARD_HTTPS_PORT = 443;
export const EMPTY_DATE = '0001-01-01T00:00:00Z'; export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300; export const DEBOUNCE_TIMEOUT = 300;
export const DEBOUNCE_FILTER_TIMEOUT = 500;
export const CHECK_TIMEOUT = 1000; export const CHECK_TIMEOUT = 1000;
export const STOP_TIMEOUT = 10000; export const STOP_TIMEOUT = 10000;
@ -379,3 +380,9 @@ export const DEFAULT_LOGS_FILTER = {
}; };
export const DEFAULT_LANGUAGE = 'en'; export const DEFAULT_LANGUAGE = 'en';
export const TABLE_DEFAULT_PAGE_SIZE = 100;
export const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};

View File

@ -20,15 +20,43 @@ const queryLogs = handleActions(
}; };
}, },
[actions.setLogsFilter]: (state, { payload }) => ( [actions.setLogsPage]: (state, { payload }) => ({
{ ...state, filter: payload } ...state,
), page: payload,
}),
[actions.setLogsFilterRequest]: state => ({ ...state, processingGetLogs: true }),
[actions.setLogsFilterFailure]: state => ({ ...state, processingGetLogs: false }),
[actions.setLogsFilterSuccess]: (state, { payload }) => {
const { logs, oldest, filter } = payload;
const pageSize = 100;
const page = 0;
const pages = Math.ceil(logs.length / pageSize);
const total = logs.length;
const rowsStart = pageSize * page;
const rowsEnd = (pageSize * page) + pageSize;
const logsSlice = logs.slice(rowsStart, rowsEnd);
const isFiltered = Object.keys(filter).some(key => filter[key]);
return {
...state,
oldest,
filter,
isFiltered,
pages,
total,
logs: logsSlice,
allLogs: logs,
processingGetLogs: false,
};
},
[actions.getLogsRequest]: state => ({ ...state, processingGetLogs: true }), [actions.getLogsRequest]: state => ({ ...state, processingGetLogs: true }),
[actions.getLogsFailure]: state => ({ ...state, processingGetLogs: false }), [actions.getLogsFailure]: state => ({ ...state, processingGetLogs: false }),
[actions.getLogsSuccess]: (state, { payload }) => { [actions.getLogsSuccess]: (state, { payload }) => {
const { const {
logs, oldest, older_than, page, pageSize, filtered, logs, oldest, older_than, page, pageSize,
} = payload; } = payload;
let logsWithOffset = state.allLogs.length > 0 ? state.allLogs : logs; let logsWithOffset = state.allLogs.length > 0 ? state.allLogs : logs;
let allLogs = logs; let allLogs = logs;
@ -36,9 +64,6 @@ const queryLogs = handleActions(
if (older_than) { if (older_than) {
logsWithOffset = [...state.allLogs, ...logs]; logsWithOffset = [...state.allLogs, ...logs];
allLogs = [...state.allLogs, ...logs]; allLogs = [...state.allLogs, ...logs];
} else if (filtered) {
logsWithOffset = logs;
allLogs = logs;
} }
const pages = Math.ceil(logsWithOffset.length / pageSize); const pages = Math.ceil(logsWithOffset.length / pageSize);
@ -91,11 +116,13 @@ const queryLogs = handleActions(
logs: [], logs: [],
interval: 1, interval: 1,
allLogs: [], allLogs: [],
page: 0,
pages: 0, pages: 0,
total: 0, total: 0,
enabled: true, enabled: true,
oldest: '', oldest: '',
filter: DEFAULT_LOGS_FILTER, filter: DEFAULT_LOGS_FILTER,
isFiltered: false,
}, },
); );