diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 53f269a2..4365b6ec 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -401,4 +401,4 @@ "descr": "Description", "whois": "Whois", "filtering_rules_learn_more": "<0>Learn more</0> about creating your own hosts blocklists." -} \ No newline at end of file +} diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index ce6b5a0c..35b4f7af 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -3,6 +3,7 @@ import { createAction } from 'redux-actions'; import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './index'; import { normalizeLogs } from '../helpers/helpers'; +import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants'; const getLogsWithParams = async (config) => { const { older_than, filter, ...values } = config; @@ -15,6 +16,41 @@ const getLogsWithParams = async (config) => { }; }; +export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUEST'); +export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE'); +export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); + +const checkFilteredLogs = async (data, filter, dispatch, total) => { + const { logs, oldest } = data; + const totalData = total || { logs }; + + const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE || + totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE) && + oldest !== ''; + + if (needToGetAdditionalLogs) { + dispatch(getAdditionalLogsRequest()); + + try { + const additionalLogs = await getLogsWithParams({ older_than: oldest, filter }); + if (additionalLogs.logs.length > 0) { + return await checkFilteredLogs(additionalLogs, filter, dispatch, { + logs: [...totalData.logs, ...additionalLogs.logs], + oldest: additionalLogs.oldest, + }); + } + dispatch(getAdditionalLogsSuccess()); + return totalData; + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getAdditionalLogsFailure(error)); + } + } + + dispatch(getAdditionalLogsSuccess()); + return totalData; +}; + export const setLogsPagination = createAction('LOGS_PAGINATION'); export const setLogsPage = createAction('SET_LOG_PAGE'); @@ -22,11 +58,20 @@ export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); -export const getLogs = config => async (dispatch) => { +export const getLogs = config => async (dispatch, getState) => { dispatch(getLogsRequest()); try { - const logs = await getLogsWithParams(config); - dispatch(getLogsSuccess(logs)); + const { isFiltered, filter, page } = getState().queryLogs; + const data = await getLogsWithParams({ ...config, filter }); + + if (isFiltered) { + const additionalData = await checkFilteredLogs(data, filter, dispatch); + const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; + dispatch(getLogsSuccess(updatedData)); + dispatch(setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE })); + } else { + dispatch(getLogsSuccess(data)); + } } catch (error) { dispatch(addErrorToast({ error })); dispatch(getLogsFailure(error)); @@ -40,8 +85,11 @@ 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)); + const data = await getLogsWithParams({ older_than: '', filter }); + const additionalData = await checkFilteredLogs(data, filter, dispatch); + const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; + + dispatch(setLogsFilterSuccess({ ...updatedData, filter })); dispatch(setLogsPage(0)); } catch (error) { dispatch(addErrorToast({ error })); diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index 9b175fc9..cedc6f5e 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -49,10 +49,10 @@ const Form = (props) => { return ( <form onSubmit={handleChange}> <div className="row"> - <div className="col-3"> + <div className="col-6 col-sm-3 my-2"> <Field - id="domain" - name="domain" + id="filter_domain" + name="filter_domain" component={renderFilterField} type="text" className="form-control" @@ -61,10 +61,10 @@ const Form = (props) => { onChange={handleChange} /> </div> - <div className="col-3"> + <div className="col-6 col-sm-3 my-2"> <Field - id="type" - name="type" + id="filter_question_type" + name="filter_question_type" component={renderField} type="text" className="form-control" @@ -72,9 +72,9 @@ const Form = (props) => { onChange={handleChange} /> </div> - <div className="col-3"> + <div className="col-6 col-sm-3 my-2"> <Field - name="response" + name="filter_response_status" component="select" className="form-control custom-select" > @@ -86,10 +86,10 @@ const Form = (props) => { </option> </Field> </div> - <div className="col-3"> + <div className="col-6 col-sm-3 my-2"> <Field - id="client" - name="client" + id="filter_client" + name="filter_client" component={renderFilterField} type="text" className="form-control" diff --git a/client/src/components/Logs/Filters/index.js b/client/src/components/Logs/Filters/index.js index 20102397..ab654cbe 100644 --- a/client/src/components/Logs/Filters/index.js +++ b/client/src/components/Logs/Filters/index.js @@ -1,24 +1,22 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash/debounce'; +import classnames from 'classnames'; import { DEBOUNCE_FILTER_TIMEOUT, RESPONSE_FILTER } from '../../../helpers/constants'; import { isValidQuestionType } from '../../../helpers/helpers'; import Form from './Form'; +import Card from '../../ui/Card'; 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 : '', - }; - }; + getFilters = ({ + filter_domain, filter_question_type, filter_response_status, filter_client, + }) => ({ + filter_domain: filter_domain || '', + filter_question_type: isValidQuestionType(filter_question_type) ? filter_question_type.toUpperCase() : '', + filter_response_status: filter_response_status === RESPONSE_FILTER.FILTERED ? filter_response_status : '', + filter_client: filter_client || '', + }); handleFormChange = debounce((values) => { const filter = this.getFilters(values); @@ -26,13 +24,20 @@ class Filters extends Component { }, DEBOUNCE_FILTER_TIMEOUT); render() { - const { filter } = this.props; + const { filter, processingAdditionalLogs } = this.props; + + const cardBodyClass = classnames({ + 'card-body': true, + 'card-body--loading': processingAdditionalLogs, + }); return ( - <Form - initialValues={filter} - onChange={this.handleFormChange} - /> + <Card bodyType={cardBodyClass}> + <Form + initialValues={filter} + onChange={this.handleFormChange} + /> + </Card> ); } } @@ -40,6 +45,8 @@ class Filters extends Component { Filters.propTypes = { filter: PropTypes.object.isRequired, setLogsFilter: PropTypes.func.isRequired, + processingGetLogs: PropTypes.bool.isRequired, + processingAdditionalLogs: PropTypes.bool.isRequired, }; export default Filters; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index d1604525..68b9cc61 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -10,7 +10,7 @@ import { formatTime, formatDateTime, } from '../../helpers/helpers'; -import { SERVICES, FILTERED_STATUS, DEFAULT_LOGS_FILTER, RESPONSE_FILTER, TABLE_DEFAULT_PAGE_SIZE } from '../../helpers/constants'; +import { SERVICES, FILTERED_STATUS, TABLE_DEFAULT_PAGE_SIZE } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; import { formatClientCell } from '../../helpers/formatClientCell'; @@ -23,7 +23,7 @@ import Popover from '../ui/Popover'; import './Logs.css'; const TABLE_FIRST_PAGE = 0; -const INITIAL_REQUEST_DATA = ['', DEFAULT_LOGS_FILTER, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE]; +const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE]; const FILTERED_REASON = 'Filtered'; class Logs extends Component { @@ -35,10 +35,10 @@ class Logs extends Component { this.props.getLogsConfig(); } - getLogs = (older_than, filter, page) => { + getLogs = (older_than, page) => { if (this.props.queryLogs.enabled) { this.props.getLogs({ - older_than, filter, page, pageSize: TABLE_DEFAULT_PAGE_SIZE, + older_than, page, pageSize: TABLE_DEFAULT_PAGE_SIZE, }); } }; @@ -218,18 +218,19 @@ class Logs extends Component { fetchData = (state) => { const { pages } = state; - const { - filter, oldest, page, - } = this.props.queryLogs; + const { oldest, page } = this.props.queryLogs; const isLastPage = pages && (page + 1 === pages); if (isLastPage) { - this.getLogs(oldest, filter, page); - } else { - this.props.setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE }); + this.getLogs(oldest, page); } }; + changePage = (page) => { + this.props.setLogsPage(page); + this.props.setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE }); + }; + renderLogs() { const { queryLogs, dashboard, t } = this.props; const { processingClients } = dashboard; @@ -250,7 +251,6 @@ class Logs extends Component { accessor: 'domain', minWidth: 180, Cell: this.getDomainCell, - Filter: this.getFilterInput, }, { Header: t('type_table_header'), @@ -262,28 +262,6 @@ class Logs extends Component { accessor: 'response', minWidth: 250, Cell: this.getResponseCell, - filterMethod: (filter, row) => { - if (filter.value === RESPONSE_FILTER.FILTERED) { - // eslint-disable-next-line no-underscore-dangle - const { reason } = row._original; - return this.checkFiltered(reason) || this.checkWhiteList(reason); - } - return true; - }, - Filter: ({ filter, onChange }) => ( - <select - className="form-control custom-select" - onChange={event => onChange(event.target.value)} - value={filter ? filter.value : RESPONSE_FILTER.ALL} - > - <option value={RESPONSE_FILTER.ALL}> - <Trans>show_all_filter_type</Trans> - </option> - <option value={RESPONSE_FILTER.FILTERED}> - <Trans>show_filtered_type</Trans> - </option> - </select> - ), }, { Header: t('client_table_header'), @@ -291,7 +269,6 @@ class Logs extends Component { maxWidth: 240, minWidth: 240, Cell: this.getClientCell, - Filter: this.getFilterInput, }, ]; @@ -311,7 +288,7 @@ class Logs extends Component { showPageJump={false} showPageSizeOptions={false} onFetchData={this.fetchData} - onPageChange={newPage => this.props.setLogsPage(newPage)} + onPageChange={this.changePage} className="logs__table" defaultPageSize={TABLE_DEFAULT_PAGE_SIZE} previousText={t('previous_btn')} @@ -365,7 +342,9 @@ class Logs extends Component { render() { const { queryLogs, t } = this.props; - const { enabled, processingGetConfig } = queryLogs; + const { + enabled, processingGetConfig, processingAdditionalLogs, processingGetLogs, + } = queryLogs; const refreshButton = enabled ? ( <button @@ -387,12 +366,12 @@ class Logs extends Component { {enabled && processingGetConfig && <Loading />} {enabled && !processingGetConfig && ( <Fragment> - <Card> - <Filters - filter={queryLogs.filter} - setLogsFilter={this.props.setLogsFilter} - /> - </Card> + <Filters + filter={queryLogs.filter} + processingGetLogs={processingGetLogs} + processingAdditionalLogs={processingAdditionalLogs} + setLogsFilter={this.props.setLogsFilter} + /> <Card>{this.renderLogs()}</Card> </Fragment> )} diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css index 6794a791..176b0160 100644 --- a/client/src/components/ui/Card.css +++ b/client/src/components/ui/Card.css @@ -33,6 +33,36 @@ text-align: center; } +.card-body--loading { + position: relative; +} + +.card-body--loading:before { + content: ""; + position: absolute; + top: 0; + left: 0; + z-index: 100; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.6); +} + +.card-body--loading:after { + content: ""; + position: absolute; + z-index: 101; + left: 50%; + top: 50%; + width: 40px; + height: 40px; + margin-top: -20px; + margin-left: -20px; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E"); + will-change: transform; + animation: clockwise 2s linear infinite; +} + .card-title-stats { font-size: 13px; color: #9aa0ac; diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index 4f883353..ee1fc91b 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -107,12 +107,23 @@ const queryLogs = handleActions( ...payload, processingSetConfig: false, }), + + [actions.getAdditionalLogsRequest]: state => ({ + ...state, processingAdditionalLogs: true, processingGetLogs: true, + }), + [actions.getAdditionalLogsFailure]: state => ({ + ...state, processingAdditionalLogs: false, processingGetLogs: false, + }), + [actions.getAdditionalLogsSuccess]: state => ({ + ...state, processingAdditionalLogs: false, processingGetLogs: false, + }), }, { processingGetLogs: true, processingClear: false, processingGetConfig: false, processingSetConfig: false, + processingAdditionalLogs: false, logs: [], interval: 1, allLogs: [],