diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index 29f5bc45..ce6b5a0c 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -4,8 +4,19 @@ import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './index'; 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 setLogsFilter = createAction('LOGS_FILTER'); +export const setLogsPage = createAction('SET_LOG_PAGE'); export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsFailure = createAction('GET_LOGS_FAILURE'); @@ -14,19 +25,30 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); export const getLogs = config => async (dispatch) => { dispatch(getLogsRequest()); try { - const { filter, older_than } = config; - const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); - const { data, oldest } = rawLogs; - const logs = normalizeLogs(data); - dispatch(getLogsSuccess({ - logs, oldest, filter, ...config, - })); + const logs = await getLogsWithParams(config); + dispatch(getLogsSuccess(logs)); } catch (error) { dispatch(addErrorToast({ 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 clearLogsFailure = createAction('CLEAR_LOGS_FAILURE'); export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS'); diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js new file mode 100644 index 00000000..9b175fc9 --- /dev/null +++ b/client/src/components/Logs/Filters/Form.js @@ -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 }, +}) => ( + +
+ + + + + {!disabled && + touched && + (error && {error})} +
+
+); + +const Form = (props) => { + const { + t, + handleChange, + } = props; + + return ( +
+
+
+ +
+
+ +
+
+ + + + +
+
+ +
+
+
+ ); +}; + +Form.propTypes = { + handleChange: PropTypes.func, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ + form: 'logsFilterForm', + }), +])(Form); diff --git a/client/src/components/Logs/Filters/index.js b/client/src/components/Logs/Filters/index.js new file mode 100644 index 00000000..20102397 --- /dev/null +++ b/client/src/components/Logs/Filters/index.js @@ -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 ( +
+ ); + } +} + +Filters.propTypes = { + filter: PropTypes.object.isRequired, + setLogsFilter: PropTypes.func.isRequired, +}; + +export default Filters; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 421f423e..d1604525 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -5,46 +5,40 @@ import escapeRegExp from 'lodash/escapeRegExp'; import endsWith from 'lodash/endsWith'; import { Trans, withNamespaces } from 'react-i18next'; import { HashLink as Link } from 'react-router-hash-link'; -import debounce from 'lodash/debounce'; import { formatTime, formatDateTime, - isValidQuestionType, } 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 { formatClientCell } from '../../helpers/formatClientCell'; +import Filters from './Filters'; import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; import Loading from '../ui/Loading'; import PopoverFiltered from '../ui/PopoverFilter'; import Popover from '../ui/Popover'; -import Tooltip from '../ui/Tooltip'; import './Logs.css'; 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 FILTERED_REASON = 'Filtered'; -const RESPONSE_FILTER = { - ALL: 'all', - FILTERED: 'filtered', -}; class Logs extends Component { componentDidMount() { + this.props.setLogsPage(TABLE_FIRST_PAGE); this.getLogs(...INITIAL_REQUEST_DATA); this.props.getFilteringStatus(); this.props.getClients(); this.props.getLogsConfig(); } - getLogs = (older_than, filter, page, pageSize, filtered) => { + getLogs = (older_than, filter, page) => { if (this.props.queryLogs.enabled) { 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(); }; - handleLogsFiltering = debounce((older_than, filter, page, pageSize, filtered) => { - this.props.getLogs({ - older_than, - filter, - page, - pageSize, - filtered, - }); - }, DEBOUNCE_TIMEOUT); - renderTooltip = (isFiltered, rule, filter, service) => isFiltered && ; @@ -232,70 +216,25 @@ class Logs extends Component { ); }; - getFilterInput = ({ filter, onChange }) => ( - -
- onChange(event.target.value)} - value={filter ? filter.value : ''} - /> - - - -
-
- ); - - 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) => { - const { pageSize, page, pages } = state; - const { filter, oldest } = this.props.queryLogs; + const { pages } = state; + const { + filter, oldest, page, + } = this.props.queryLogs; const isLastPage = pages && (page + 1 === pages); if (isLastPage) { - this.getLogs(oldest, filter, page, pageSize, false); + this.getLogs(oldest, filter, page); } 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 ( - - {pages || 1}{showEllipsis && '…' } - - ); - } - renderLogs() { const { queryLogs, dashboard, t } = this.props; const { processingClients } = dashboard; const { - processingGetLogs, processingGetConfig, logs, pages, + processingGetLogs, processingGetConfig, logs, pages, page, } = queryLogs; const isLoading = processingGetLogs || processingClients || processingGetConfig; @@ -304,7 +243,6 @@ class Logs extends Component { Header: t('time_table_header'), accessor: 'time', maxWidth: 100, - filterable: false, Cell: this.getTimeCell, }, { @@ -360,18 +298,20 @@ class Logs extends Component { return ( this.props.setLogsPage(newPage)} className="logs__table" defaultPageSize={TABLE_DEFAULT_PAGE_SIZE} previousText={t('previous_btn')} @@ -381,7 +321,6 @@ class Logs extends Component { noDataText={t('no_logs_found')} pageText={''} ofText={''} - renderCurrentPage={() => false} renderTotalPagesCount={() => false} defaultFilterMethod={(filter, row) => { const id = filter.pivotId || filter.id; @@ -446,7 +385,17 @@ class Logs extends Component { {refreshButton} {enabled && processingGetConfig && } - {enabled && !processingGetConfig && {this.renderLogs()}} + {enabled && !processingGetConfig && ( + + + + + {this.renderLogs()} + + )} {!enabled && !processingGetConfig && (
@@ -479,6 +428,7 @@ Logs.propTypes = { getLogsConfig: PropTypes.func.isRequired, setLogsPagination: PropTypes.func.isRequired, setLogsFilter: PropTypes.func.isRequired, + setLogsPage: PropTypes.func.isRequired, t: PropTypes.func.isRequired, }; diff --git a/client/src/components/ui/Tooltip.css b/client/src/components/ui/Tooltip.css index 27505a68..9ad8af3b 100644 --- a/client/src/components/ui/Tooltip.css +++ b/client/src/components/ui/Tooltip.css @@ -64,6 +64,7 @@ top: calc(100% + 10px); right: -10px; left: initial; + width: 255px; transform: none; } diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js index 3b93ec94..be328fbd 100644 --- a/client/src/containers/Logs.js +++ b/client/src/containers/Logs.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { addSuccessToast, getClients } from '../actions'; 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'; const mapStateToProps = (state) => { @@ -19,6 +19,7 @@ const mapDispatchToProps = { getLogsConfig, setLogsPagination, setLogsFilter, + setLogsPage, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index fb82c1c9..ca19818e 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -141,6 +141,7 @@ export const STANDARD_HTTPS_PORT = 443; export const EMPTY_DATE = '0001-01-01T00:00:00Z'; export const DEBOUNCE_TIMEOUT = 300; +export const DEBOUNCE_FILTER_TIMEOUT = 500; export const CHECK_TIMEOUT = 1000; export const STOP_TIMEOUT = 10000; @@ -379,3 +380,9 @@ export const DEFAULT_LOGS_FILTER = { }; export const DEFAULT_LANGUAGE = 'en'; + +export const TABLE_DEFAULT_PAGE_SIZE = 100; +export const RESPONSE_FILTER = { + ALL: 'all', + FILTERED: 'filtered', +}; diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index ff59426f..4f883353 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -20,15 +20,43 @@ const queryLogs = handleActions( }; }, - [actions.setLogsFilter]: (state, { payload }) => ( - { ...state, filter: payload } - ), + [actions.setLogsPage]: (state, { 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.getLogsFailure]: state => ({ ...state, processingGetLogs: false }), [actions.getLogsSuccess]: (state, { payload }) => { const { - logs, oldest, older_than, page, pageSize, filtered, + logs, oldest, older_than, page, pageSize, } = payload; let logsWithOffset = state.allLogs.length > 0 ? state.allLogs : logs; let allLogs = logs; @@ -36,9 +64,6 @@ const queryLogs = handleActions( if (older_than) { logsWithOffset = [...state.allLogs, ...logs]; allLogs = [...state.allLogs, ...logs]; - } else if (filtered) { - logsWithOffset = logs; - allLogs = logs; } const pages = Math.ceil(logsWithOffset.length / pageSize); @@ -91,11 +116,13 @@ const queryLogs = handleActions( logs: [], interval: 1, allLogs: [], + page: 0, pages: 0, total: 0, enabled: true, oldest: '', filter: DEFAULT_LOGS_FILTER, + isFiltered: false, }, );