diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index e4e1c1af..d3700869 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -163,7 +163,6 @@ "show_filtered_type": "Show filtered", "no_logs_found": "No logs found", "refresh_btn": "Refresh", - "last_dns_queries": "Last 5000 DNS queries", "previous_btn": "Previous", "next_btn": "Next", "loading_table_status": "Loading...", @@ -182,6 +181,7 @@ "query_log_enable": "Enable log", "query_log_configuration": "Logs configuration", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings0>", + "query_log_strict_search": "Use double quotes for strict search", "source_label": "Source", "found_in_known_domain_db": "Found in the known domains database.", "category_label": "Category", diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index c68ddf15..f36bc3d1 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -4,15 +4,18 @@ import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './index'; import { normalizeLogs } from '../helpers/helpers'; +export const setLogsPagination = createAction('LOGS_PAGINATION'); + export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); -export const getLogs = () => async (dispatch) => { +export const getLogs = config => async (dispatch) => { dispatch(getLogsRequest()); try { - const logs = normalizeLogs(await apiClient.getQueryLog()); - dispatch(getLogsSuccess(logs)); + const { filter, lastRowTime: older_than } = config; + const logs = normalizeLogs(await apiClient.getQueryLog({ filter, older_than })); + dispatch(getLogsSuccess({ logs, ...config })); } catch (error) { dispatch(addErrorToast({ error })); dispatch(getLogsFailure(error)); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index b7a7d045..9cd0e650 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -482,14 +482,18 @@ class Api { } // Query log - GET_QUERY_LOG = { path: 'querylog', method: 'GET' }; + GET_QUERY_LOG = { path: 'querylog', method: 'POST' }; QUERY_LOG_CONFIG = { path: 'querylog_config', method: 'POST' }; QUERY_LOG_INFO = { path: 'querylog_info', method: 'GET' }; QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' }; - getQueryLog() { + getQueryLog(data) { const { path, method } = this.GET_QUERY_LOG; - return this.makeRequest(path, method); + const config = { + data, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, config); } getQueryLogInfo() { diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 931e8694..5a79ed85 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -107,6 +107,11 @@ border: 1px solid rgba(0, 40, 100, 0.12); } +.logs__table .rt-thead.-filters select { + background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center; + background-size: 8px 10px; +} + .logs__table .rt-thead.-filters input:focus, .logs__table .rt-thead.-filters select:focus { border-color: #1991eb; @@ -130,6 +135,21 @@ overflow: hidden; } +.logs__input-wrap { + position: relative; +} + +.logs__notice { + position: absolute; + z-index: 1; + top: 8px; + right: 10px; + margin-top: 3px; + font-size: 12px; + text-align: left; + color: #a5a5a5; +} + .logs__whois { display: inline; } diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 994a4e18..eeadb6fb 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -5,9 +5,14 @@ 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 } from '../../helpers/helpers'; -import { SERVICES, FILTERED_STATUS } from '../../helpers/constants'; +import { + formatTime, + formatDateTime, + isValidQuestionType, +} from '../../helpers/helpers'; +import { SERVICES, FILTERED_STATUS, DEBOUNCE_TIMEOUT } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; import { formatClientCell } from '../../helpers/formatClientCell'; @@ -16,8 +21,12 @@ 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 = 50; +const INITIAL_REQUEST_DATA = ['', {}, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE]; const FILTERED_REASON = 'Filtered'; const RESPONSE_FILTER = { ALL: 'all', @@ -26,26 +35,36 @@ const RESPONSE_FILTER = { class Logs extends Component { componentDidMount() { - this.getLogs(); + this.getLogs(...INITIAL_REQUEST_DATA); this.props.getFilteringStatus(); this.props.getClients(); this.props.getLogsConfig(); } - componentDidUpdate(prevProps) { - // get logs when queryLog becomes enabled - if (this.props.queryLogs.enabled && !prevProps.queryLogs.enabled) { - this.props.getLogs(); - } - } - - getLogs = () => { - // get logs on initialization if queryLogIsEnabled + getLogs = (lastRowTime, filter, page, pageSize) => { if (this.props.queryLogs.enabled) { - this.props.getLogs(); + this.props.getLogs({ + lastRowTime, filter, page, pageSize, + }); } }; + refreshLogs = (lastRowTime, filter, page, pageSize, refreshLogs = true) => { + this.props.getLogs({ + lastRowTime, filter, page, pageSize, refreshLogs, + }); + }; + + handleLogsFiltering = debounce((lastRowTime, filter, page, pageSize, filtered) => { + this.props.getLogs({ + lastRowTime, + filter, + page, + pageSize, + filtered, + }); + }, DEBOUNCE_TIMEOUT); + renderTooltip = (isFiltered, rule, filter, service) => isFiltered && ; @@ -215,8 +234,64 @@ class Logs extends Component { ); }; - renderLogs(logs) { - const { t } = this.props; + 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 { + domain: domain || '', + client: client || '', + question_type: isValidQuestionType(type) ? type.toUpperCase() : '', + response_status: response === RESPONSE_FILTER.FILTERED ? response : '', + }; + }; + + fetchData = (state) => { + const { + filtered, pageSize, page, pages, + } = state; + const { allLogs } = this.props.queryLogs; + const isLastPage = pages && (page + 1 >= pages); + const isFiltersPresent = filtered.length > 0; + const filter = this.getFilters(filtered); + + if (isFiltersPresent) { + this.handleLogsFiltering('', filter, page, pageSize, true); + } else if (isLastPage) { + const lastRow = allLogs[allLogs.length - 1]; + const lastRowTime = (lastRow && lastRow.time) || ''; + this.getLogs(lastRowTime, filter, page, pageSize); + } else { + this.props.setLogsPagination({ page, pageSize }); + } + }; + + renderLogs() { + const { queryLogs, dashboard, t } = this.props; + const { processingClients } = dashboard; + const { + processingGetLogs, processingGetConfig, logs, pages, + } = queryLogs; + const isLoading = processingGetLogs || processingClients || processingGetConfig; + const columns = [ { Header: t('time_table_header'), @@ -230,6 +305,7 @@ class Logs extends Component { accessor: 'domain', minWidth: 180, Cell: this.getDomainCell, + Filter: this.getFilterInput, }, { Header: t('type_table_header'), @@ -251,7 +327,7 @@ class Logs extends Component { }, Filter: ({ filter, onChange }) => ( onChange(event.target.value)} value={filter ? filter.value : RESPONSE_FILTER.ALL} > @@ -270,82 +346,83 @@ class Logs extends Component { maxWidth: 240, minWidth: 240, Cell: this.getClientCell, + Filter: this.getFilterInput, }, ]; - if (logs) { - return ( - { - const id = filter.pivotId || filter.id; - return row[id] !== undefined - ? String(row[id]).indexOf(filter.value) !== -1 - : true; - }} - defaultSorted={[ - { - id: 'time', - desc: true, - }, - ]} - getTrProps={(_state, rowInfo) => { - if (!rowInfo) { - return {}; - } + return ( + this.setState({ page })} + className="logs__table" + showPagination={true} + defaultPageSize={TABLE_DEFAULT_PAGE_SIZE} + previousText={t('previous_btn')} + nextText={t('next_btn')} + loadingText={t('loading_table_status')} + pageText={t('page_table_footer_text')} + ofText={t('of_table_footer_text')} + rowsText={t('rows_table_footer_text')} + noDataText={t('no_logs_found')} + defaultFilterMethod={(filter, row) => { + const id = filter.pivotId || filter.id; + return row[id] !== undefined + ? String(row[id]).indexOf(filter.value) !== -1 + : true; + }} + defaultSorted={[ + { + id: 'time', + desc: true, + }, + ]} + getTrProps={(_state, rowInfo) => { + if (!rowInfo) { + return {}; + } - const { reason } = rowInfo.original; - - if (this.checkFiltered(reason)) { - return { - className: 'red', - }; - } else if (this.checkWhiteList(reason)) { - return { - className: 'green', - }; - } else if (this.checkRewrite(reason)) { - return { - className: 'blue', - }; - } + const { reason } = rowInfo.original; + if (this.checkFiltered(reason)) { return { - className: '', + className: 'red', }; - }} - /> - ); - } + } else if (this.checkWhiteList(reason)) { + return { + className: 'green', + }; + } else if (this.checkRewrite(reason)) { + return { + className: 'blue', + }; + } - return null; + return { + className: '', + }; + }} + /> + ); } render() { - const { queryLogs, dashboard, t } = this.props; - const { enabled, processingGetLogs, processingGetConfig } = queryLogs; - const { processingClients } = dashboard; - const isDataReady = - !processingGetLogs && !processingGetConfig && !dashboard.processingClients; + const { queryLogs, t } = this.props; + const { enabled, processingGetConfig } = queryLogs; const refreshButton = enabled ? ( this.refreshLogs(...INITIAL_REQUEST_DATA)} > @@ -357,22 +434,24 @@ class Logs extends Component { return ( - - {refreshButton} - - - {enabled && (processingGetLogs || processingClients || processingGetConfig) && ( - - )} - {enabled && isDataReady && this.renderLogs(queryLogs.logs)} - {!enabled && !processingGetConfig && ( + {refreshButton} + {enabled && processingGetConfig && } + {enabled && !processingGetConfig && {this.renderLogs()}} + {!enabled && !processingGetConfig && ( + - link]}> + + link + , + ]} + > query_log_disabled - )} - + + )} ); } @@ -388,6 +467,7 @@ Logs.propTypes = { addSuccessToast: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired, getLogsConfig: PropTypes.func.isRequired, + setLogsPagination: PropTypes.func.isRequired, t: PropTypes.func.isRequired, }; diff --git a/client/src/components/ui/Popover.css b/client/src/components/ui/Popover.css index 707affbc..250848af 100644 --- a/client/src/components/ui/Popover.css +++ b/client/src/components/ui/Popover.css @@ -38,6 +38,7 @@ position: absolute; bottom: calc(100% + 3px); left: 50%; + z-index: 1; min-width: 275px; padding: 10px 15px; font-size: 0.8rem; diff --git a/client/src/components/ui/Tooltip.css b/client/src/components/ui/Tooltip.css index 140a16d5..27505a68 100644 --- a/client/src/components/ui/Tooltip.css +++ b/client/src/components/ui/Tooltip.css @@ -52,3 +52,23 @@ .tooltip-custom--narrow:before { width: 220px; } + +.tooltip-custom--logs { + border-radius: 50%; + background-image: url("./svg/help-circle-gray.svg"); + background-color: #fff; +} + +.tooltip-custom--logs:before { + bottom: initial; + top: calc(100% + 10px); + right: -10px; + left: initial; + transform: none; +} + +.tooltip-custom--logs:after { + top: 8px; + border-top: none; + border-bottom: 6px solid #585965; +} diff --git a/client/src/components/ui/svg/help-circle-gray.svg b/client/src/components/ui/svg/help-circle-gray.svg new file mode 100644 index 00000000..ffd698a1 --- /dev/null +++ b/client/src/components/ui/svg/help-circle-gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js index c512f495..5d3e6f6c 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 } from '../actions/queryLogs'; +import { getLogs, getLogsConfig, setLogsPagination } from '../actions/queryLogs'; import Logs from '../components/Logs'; const mapStateToProps = (state) => { @@ -17,6 +17,7 @@ const mapDispatchToProps = { addSuccessToast, getClients, getLogsConfig, + setLogsPagination, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 8c227dac..b6148b0c 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -274,3 +274,47 @@ export const WHOIS_ICONS = { netname: 'network', descr: '', }; + +export const DNS_RECORD_TYPES = [ + 'A', + 'AAAA', + 'AFSDB', + 'APL', + 'CAA', + 'CDNSKEY', + 'CDS', + 'CERT', + 'CNAME', + 'CSYNC', + 'DHCID', + 'DLV', + 'DNAME', + 'DNSKEY', + 'DS', + 'HIP', + 'IPSECKEY', + 'KEY', + 'KX', + 'LOC', + 'MX', + 'NAPTR', + 'NS', + 'NSEC', + 'NSEC3', + 'NSEC3PARAM', + 'OPENPGPKEY', + 'PTR', + 'RRSIG', + 'RP', + 'SIG', + 'SMIMEA', + 'SOA', + 'SRV', + 'SSHFP', + 'TA', + 'TKEY', + 'TLSA', + 'TSIG', + 'TXT', + 'URI', +]; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 7ded995a..ebb96923 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -14,6 +14,7 @@ import { STANDARD_WEB_PORT, STANDARD_HTTPS_PORT, CHECK_TIMEOUT, + DNS_RECORD_TYPES, } from './constants'; export const formatTime = (time) => { @@ -318,3 +319,5 @@ export const normalizeWhois = (whois) => { return whois; }; + +export const isValidQuestionType = type => type && DNS_RECORD_TYPES.includes(type.toUpperCase()); diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index 3dfbadff..29781f25 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -4,11 +4,54 @@ import * as actions from '../actions/queryLogs'; const queryLogs = handleActions( { + [actions.setLogsPagination]: (state, { payload }) => { + const { page, pageSize } = payload; + const { allLogs } = state; + const rowsStart = pageSize * page; + const rowsEnd = (pageSize * page) + pageSize; + const logsSlice = allLogs.slice(rowsStart, rowsEnd); + const pages = Math.ceil(allLogs.length / pageSize); + + return { + ...state, + pages, + logs: logsSlice, + }; + }, + [actions.getLogsRequest]: state => ({ ...state, processingGetLogs: true }), [actions.getLogsFailure]: state => ({ ...state, processingGetLogs: false }), [actions.getLogsSuccess]: (state, { payload }) => { - const newState = { ...state, logs: payload, processingGetLogs: false }; - return newState; + const { + logs, lastRowTime, page, pageSize, filtered, refreshLogs, + } = payload; + let logsWithOffset = state.allLogs.length > 0 ? state.allLogs : logs; + let allLogs = logs; + + if (lastRowTime) { + logsWithOffset = [...state.allLogs, ...logs]; + allLogs = [...state.allLogs, ...logs]; + } + + if (filtered || refreshLogs) { + logsWithOffset = logs; + allLogs = logs; + } + + const pages = Math.ceil(logsWithOffset.length / pageSize); + const total = logsWithOffset.length; + const rowsStart = pageSize * page; + const rowsEnd = (pageSize * page) + pageSize; + const logsSlice = logsWithOffset.slice(rowsStart, rowsEnd); + + return { + ...state, + pages, + total, + allLogs, + logs: logsSlice, + processingGetLogs: false, + }; }, [actions.clearLogsRequest]: state => ({ ...state, processingClear: true }), @@ -42,6 +85,10 @@ const queryLogs = handleActions( processingSetConfig: false, logs: [], interval: 1, + allLogs: [], + pages: 0, + offset: 0, + total: 0, enabled: true, }, );