From da4a1ec23d30a7c1ff3d70f943bb9d29ac7feeea Mon Sep 17 00:00:00 2001 From: Artem Baskal Date: Mon, 13 Jul 2020 16:06:56 +0300 Subject: [PATCH] +client: "Drill down" to activity reports Close #1625 Squashed commit of the following: commit a01f12c4e5831c43dbe3ae8a80f4db12077dbb2a Author: ArtemBaskal Date: Mon Jul 13 15:50:15 2020 +0300 minor commit b8ceb17a3b12e47de81af85fa30c2961a4a42fab Merge: 702c55ed fecf5494 Author: Andrey Meshkov Date: Mon Jul 13 15:32:44 2020 +0300 Merge branch 'feature/1625' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/1625 commit 702c55edc1ba2ab330eda8189498dfff33c92f5f Author: Andrey Meshkov Date: Mon Jul 13 15:32:41 2020 +0300 fix makefile when there's no gopath commit fecf5494b8c1719cb70044f336fe99c341802d25 Merge: d4c811f9 8a417604 Author: ArtemBaskal Date: Mon Jul 13 15:30:21 2020 +0300 Merge branch 'master' into feature/1625 commit d4c811f9630dee448012434e2f50f34ab8b8b899 Merge: b0a037da a33164bf Author: ArtemBaskal Date: Mon Jul 13 12:35:16 2020 +0300 Merge branch 'master' into feature/1625 commit b0a037daf48913fd8a4cda16d520835630072520 Author: ArtemBaskal Date: Mon Jul 13 12:34:42 2020 +0300 Simplify sync logs action creators commit eeeb620ae100a554f59783fc2a14fad525ce1a82 Author: ArtemBaskal Date: Mon Jul 13 11:17:08 2020 +0300 Review changes commit 4cbc59eec5c794df18d6cb9b33f39091ce7cfde9 Author: ArtemBaskal Date: Fri Jul 10 15:23:37 2020 +0300 Update tracker tooltip class commit 0a705301d4726af1c8f7f7a5776b11d338ab1d54 Author: ArtemBaskal Date: Fri Jul 10 13:46:10 2020 +0300 Replace depricated addListener commit 2ac0843239853da1725d2e038b5e4cbaef253732 Author: ArtemBaskal Date: Fri Jul 10 13:39:45 2020 +0300 Validate response_status url param commit 2178039ebbd0cbe2c0048cb5ab7ad7c7e7571bd1 Author: ArtemBaskal Date: Fri Jul 10 12:58:18 2020 +0300 Fix setting empty search value, use strict search on drill down, extract refreshFilteredLogs action commit 4b11c6a34049bd133077bad035d267f87cdec141 Author: ArtemBaskal Date: Thu Jul 9 19:41:48 2020 +0300 Normalize input search commit 3fded3575b21bdd017723f5e487c268074599e4f Author: ArtemBaskal Date: Thu Jul 9 18:20:05 2020 +0300 Optimize search commit 9073e032e4aadcdef9d826f16a10c300ee46b30e Author: ArtemBaskal Date: Thu Jul 9 14:28:41 2020 +0300 Update url string params commit a18cffc8bfac83103fb78ffae2f786f89aea8ba1 Author: ArtemBaskal Date: Thu Jul 9 12:55:50 2020 +0300 Fix reset search commit 33f769aed56369aacedd29ffd52b527b527d4a59 Author: ArtemBaskal Date: Wed Jul 8 19:13:21 2020 +0300 WIP: Add permlinks commit 4422641cf5cff06c8485ea23d58e5d42f7cca5cd Author: ArtemBaskal Date: Wed Jul 8 14:42:28 2020 +0300 Refactor Counters, add response_status links to query log commit e8bb0b70ca55f31ef3fcdda13dcaad6f5d8479b5 Author: ArtemBaskal Date: Tue Jul 7 19:33:04 2020 +0300 Delete unnecessary file commit b20816e9dad79866e3ec04d3093c972967b3b226 Merge: 6281084e d2c3af5c Author: ArtemBaskal Date: Tue Jul 7 19:30:44 2020 +0300 Resolve conflict commit d2c3af5cf227d76f876d6d94ca016d4b242b2515 Author: ArtemBaskal Date: Tue Jul 7 17:14:51 2020 +0300 + client: Add git hooks ... and 5 more commits --- Makefile | 2 +- client/package-lock.json | 67 ++++-- client/package.json | 1 + client/src/__locales/en.json | 3 +- client/src/actions/queryLogs.js | 70 +++++-- client/src/components/App/index.js | 6 +- .../components/Dashboard/BlockedDomains.js | 6 +- client/src/components/Dashboard/Clients.js | 2 +- client/src/components/Dashboard/Counters.js | 192 ++++++++---------- .../components/Dashboard/QueriedDomains.js | 6 +- client/src/components/Dashboard/index.js | 7 - client/src/components/Header/Menu.js | 137 +++++++++---- .../components/Logs/Cells/getDomainCell.js | 6 +- client/src/components/Logs/Filters/Form.js | 137 ++++++++----- client/src/components/Logs/Filters/index.js | 20 +- client/src/components/Logs/Table.js | 4 +- client/src/components/Logs/index.js | 72 +++++-- .../Settings/Clients/AutoClients.js | 5 +- .../Settings/Clients/ClientsTable.js | 17 +- client/src/components/ui/Cell.js | 36 ++-- .../ui/{Tooltip.css => IconTooltip.css} | 0 client/src/components/ui/IconTooltip.js | 19 ++ client/src/components/ui/LogsSearchLink.css | 7 + client/src/components/ui/LogsSearchLink.js | 33 +++ client/src/components/ui/Tooltip.js | 14 -- client/src/containers/Logs.js | 3 +- client/src/helpers/constants.js | 5 + client/src/helpers/helpers.js | 11 + client/src/helpers/useDebounce.js | 22 ++ client/src/reducers/queryLogs.js | 12 +- 30 files changed, 591 insertions(+), 331 deletions(-) rename client/src/components/ui/{Tooltip.css => IconTooltip.css} (100%) create mode 100644 client/src/components/ui/IconTooltip.js create mode 100644 client/src/components/ui/LogsSearchLink.css create mode 100644 client/src/components/ui/LogsSearchLink.js delete mode 100644 client/src/components/ui/Tooltip.js create mode 100644 client/src/helpers/useDebounce.js diff --git a/Makefile b/Makefile index f577ea7c..074da525 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ endif all: build build: dependencies client - go generate ./... + PATH=$(GOPATH)/bin:$(PATH) go generate ./... CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" PATH=$(GOPATH)/bin:$(PATH) packr clean diff --git a/client/package-lock.json b/client/package-lock.json index faaa5598..86fdb272 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1909,6 +1909,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -3097,6 +3102,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -4818,8 +4828,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-equal": { "version": "1.1.1", @@ -11044,6 +11053,24 @@ "prepend-http": "^1.0.0", "query-string": "^4.1.0", "sort-keys": "^1.0.0" + }, + "dependencies": { + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + } } }, "npm-run-path": { @@ -12138,13 +12165,13 @@ "dev": true }, "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.1.tgz", + "integrity": "sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==", "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" } }, "querystring": { @@ -13455,6 +13482,18 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -13696,6 +13735,11 @@ "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", "dev": true }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -13833,10 +13877,9 @@ "dev": true }, "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" }, "string-length": { "version": "4.0.1", diff --git a/client/package.json b/client/package.json index edf5e424..d462519c 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "lodash": "^4.17.15", "nanoid": "^3.1.9", "prop-types": "^15.7.2", + "query-string": "^6.13.1", "react": "^16.13.1", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index ed1148fe..c739e4a3 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -562,5 +562,6 @@ "filter_category_security_desc": "Lists that specialize on blocking malware, phishing or scam domains", "filter_category_regional_desc": "Lists that focus on regional ads and tracking servers", "filter_category_other_desc": "Other blocklists", - "original_response": "Original response" + "original_response": "Original response", + "click_to_view_queries": "Click to view queries" } diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index 2a230622..bf3bee9f 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -2,12 +2,19 @@ import { createAction } from 'redux-actions'; import apiClient from '../api/Api'; import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; -import { TABLE_DEFAULT_PAGE_SIZE, TABLE_FIRST_PAGE } from '../helpers/constants'; +import { + DEFAULT_LOGS_FILTER, + TABLE_DEFAULT_PAGE_SIZE, + TABLE_FIRST_PAGE, +} from '../helpers/constants'; import { addErrorToast, addSuccessToast } from './toasts'; const getLogsWithParams = async (config) => { const { older_than, filter, ...values } = config; - const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); + const rawLogs = await apiClient.getQueryLog({ + ...filter, + older_than, + }); const { data, oldest } = rawLogs; let logs = normalizeLogs(data); const clientsParams = getParamsForClientsSearch(logs, 'client'); @@ -18,7 +25,11 @@ const getLogsWithParams = async (config) => { } return { - logs, oldest, older_than, filter, ...values, + logs, + oldest, + older_than, + filter, + ...values, }; }; @@ -38,7 +49,10 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => { dispatch(getAdditionalLogsRequest()); try { - const additionalLogs = await getLogsWithParams({ older_than: oldest, filter }); + const additionalLogs = await getLogsWithParams({ + older_than: oldest, + filter, + }); if (additionalLogs.oldest.length > 0) { return await checkFilteredLogs(additionalLogs, filter, dispatch, { logs: [...totalData.logs, ...additionalLogs.logs], @@ -69,13 +83,19 @@ export const getLogs = (config) => async (dispatch, getState) => { dispatch(getLogsRequest()); try { const { isFiltered, filter, page } = getState().queryLogs; - const data = await getLogsWithParams({ ...config, filter }); + 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 })); + dispatch(setLogsPagination({ + page, + pageSize: TABLE_DEFAULT_PAGE_SIZE, + })); } else { dispatch(getLogsSuccess(data)); } @@ -86,24 +106,48 @@ export const getLogs = (config) => async (dispatch, getState) => { }; 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()); +/** + * + * @param filter + * @param {string} filter.search + * @param {string} filter.response_status query field of RESPONSE_FILTER object + * @returns function + */ +export const setLogsFilter = (filter) => setLogsFilterRequest(filter); + +export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST'); +export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE'); +export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS'); + +export const setFilteredLogs = (filter) => async (dispatch) => { + dispatch(setFilteredLogsRequest()); try { - const data = await getLogsWithParams({ older_than: '', filter }); + 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(setFilteredLogsSuccess({ + ...updatedData, + filter, + })); dispatch(setLogsPage(TABLE_FIRST_PAGE)); } catch (error) { dispatch(addErrorToast({ error })); - dispatch(setLogsFilterFailure(error)); + dispatch(setFilteredLogsFailure(error)); } }; +export const resetFilteredLogs = () => setFilteredLogs(DEFAULT_LOGS_FILTER); + +export const refreshFilteredLogs = () => async (dispatch, getState) => { + const { filter } = getState().queryLogs; + await dispatch(setFilteredLogs(filter)); +}; + 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/App/index.js b/client/src/components/App/index.js index afbd3888..71b33039 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -36,7 +36,7 @@ import i18n from '../../i18n'; import Loading from '../ui/Loading'; import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants'; import Services from '../Filters/Services'; -import { setHtmlLangAttr } from '../../helpers/helpers'; +import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers'; class App extends Component { componentDidMount() { @@ -111,7 +111,9 @@ class App extends Component { {!dashboard.processing && dashboard.isCoreRunning && ( <> - + diff --git a/client/src/components/Dashboard/BlockedDomains.js b/client/src/components/Dashboard/BlockedDomains.js index 1df5ab81..144f5bee 100644 --- a/client/src/components/Dashboard/BlockedDomains.js +++ b/client/src/components/Dashboard/BlockedDomains.js @@ -14,7 +14,11 @@ const CountCell = (totalBlocked) => function cell(row) { const { value } = row; const percent = getPercent(totalBlocked, value); - return ; + return ; }; const BlockedDomains = ({ diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index 8cd30234..fc129b93 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -25,7 +25,7 @@ const countCell = (dnsQueries) => function cell(row) { const percent = getPercent(dnsQueries, value); const percentColor = getClientsPercentColor(percent); - return ; + return ; }; const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => { diff --git a/client/src/components/Dashboard/Counters.js b/client/src/components/Dashboard/Counters.js index e55f168e..9f780c2e 100644 --- a/client/src/components/Dashboard/Counters.js +++ b/client/src/components/Dashboard/Counters.js @@ -1,31 +1,80 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; +import propTypes from 'prop-types'; +import { Trans, useTranslation } from 'react-i18next'; import round from 'lodash/round'; - +import { shallowEqual, useSelector } from 'react-redux'; import Card from '../ui/Card'; -import Tooltip from '../ui/Tooltip'; +import IconTooltip from '../ui/IconTooltip'; import { formatNumber } from '../../helpers/helpers'; +import LogsSearchLink from '../ui/LogsSearchLink'; +import { RESPONSE_FILTER } from '../../helpers/constants'; -const tooltipType = 'tooltip-custom--narrow'; +const Row = ({ + label, count, response_status, tooltipTitle, translationComponents, +}) => { + const content = response_status + ? {formatNumber(count)} + : count; -const Counters = (props) => { + return + + {label} + + + {content} + ; +}; + +const Counters = ({ refreshButton, subtitle }) => { const { - t, interval, - refreshButton, - subtitle, - dnsQueries, - blockedFiltering, - replacedSafebrowsing, - replacedParental, - replacedSafesearch, + numDnsQueries, + numBlockedFiltering, + numReplacedSafebrowsing, + numReplacedParental, + numReplacedSafesearch, avgProcessingTime, - } = props; + } = useSelector((state) => state.stats, shallowEqual); + const { t } = useTranslation(); - const tooltipTitle = interval === 1 - ? t('number_of_dns_query_24_hours') - : t('number_of_dns_query_days', { count: interval }); + const rows = [ + { + label: 'dns_query', + count: numDnsQueries, + tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }), + response_status: RESPONSE_FILTER.ALL.query, + }, + { + label: 'blocked_by', + count: numBlockedFiltering, + tooltipTitle: 'number_of_dns_query_blocked_24_hours', + response_status: RESPONSE_FILTER.BLOCKED.query, + translationComponents: [link], + }, + { + label: 'stats_malware_phishing', + count: numReplacedSafebrowsing, + tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec', + response_status: RESPONSE_FILTER.BLOCKED_THREATS.query, + }, + { + label: 'stats_adult', + count: numReplacedParental, + tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult', + response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.query, + }, + { + label: 'enforced_save_search', + count: numReplacedSafesearch, + tooltipTitle: 'number_of_dns_query_to_safe_search', + response_status: RESPONSE_FILTER.SAFE_SEARCH.query, + }, + { + label: 'average_processing_time', + count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0, + tooltipTitle: 'average_processing_time_hint', + }, + ]; return ( { refresh={refreshButton} > - - - - - - - - - - - - - - - - - - - - - - - - - - + {rows.map(Row)}
- dns_query - - - - {formatNumber(dnsQueries)} - -
- link]}> - blocked_by - - - - - {formatNumber(blockedFiltering)} - -
- stats_malware_phishing - - - - {formatNumber(replacedSafebrowsing)} - -
- stats_adult - - - - {formatNumber(replacedParental)} - -
- enforced_save_search - - - - {formatNumber(replacedSafesearch)} - -
- average_processing_time - - - - {avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0} - -
); }; -Counters.propTypes = { - dnsQueries: PropTypes.number.isRequired, - blockedFiltering: PropTypes.number.isRequired, - replacedSafebrowsing: PropTypes.number.isRequired, - replacedParental: PropTypes.number.isRequired, - replacedSafesearch: PropTypes.number.isRequired, - avgProcessingTime: PropTypes.number.isRequired, - refreshButton: PropTypes.node.isRequired, - subtitle: PropTypes.string.isRequired, - interval: PropTypes.number.isRequired, - t: PropTypes.func.isRequired, +Row.propTypes = { + label: propTypes.string.isRequired, + count: propTypes.string.isRequired, + response_status: propTypes.string, + tooltipTitle: propTypes.string.isRequired, + translationComponents: propTypes.arrayOf(propTypes.element), }; -export default withTranslation()(Counters); +Counters.propTypes = { + refreshButton: propTypes.node.isRequired, + subtitle: propTypes.string.isRequired, +}; + +export default Counters; diff --git a/client/src/components/Dashboard/QueriedDomains.js b/client/src/components/Dashboard/QueriedDomains.js index c9a2fb4e..debc4fe7 100644 --- a/client/src/components/Dashboard/QueriedDomains.js +++ b/client/src/components/Dashboard/QueriedDomains.js @@ -13,7 +13,8 @@ import { getPercent } from '../../helpers/helpers'; const getQueriedPercentColor = (percent) => { if (percent > 10) { return STATUS_COLORS.red; - } if (percent > 5) { + } + if (percent > 5) { return STATUS_COLORS.yellow; } return STATUS_COLORS.green; @@ -24,7 +25,8 @@ const countCell = (dnsQueries) => function cell(row) { const percent = getPercent(dnsQueries, value); const percentColor = getQueriedPercentColor(percent); - return ; + return ; }; const QueriedDomains = ({ diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 89bd5e92..71a1366b 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -111,13 +111,6 @@ class Dashboard extends Component {
diff --git a/client/src/components/Header/Menu.js b/client/src/components/Header/Menu.js index 5e5e449d..fef0dd50 100644 --- a/client/src/components/Header/Menu.js +++ b/client/src/components/Header/Menu.js @@ -1,16 +1,19 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; import enhanceWithClickOutside from 'react-click-outside'; import classnames from 'classnames'; import { Trans, withTranslation } from 'react-i18next'; - import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants'; import Dropdown from '../ui/Dropdown'; const MENU_ITEMS = [ { - route: MENU_URLS.root, exact: true, icon: 'dashboard', text: 'dashboard', order: 0, + route: MENU_URLS.root, + exact: true, + icon: 'dashboard', + text: 'dashboard', + order: 0, }, // Settings dropdown should have visual order 1 @@ -18,27 +21,63 @@ const MENU_ITEMS = [ // Filters dropdown should have visual order 2 { - route: MENU_URLS.logs, icon: 'log', text: 'query_log', order: 3, + route: MENU_URLS.logs, + icon: 'log', + text: 'query_log', + order: 3, }, { - route: MENU_URLS.guide, icon: 'setup', text: 'setup_guide', order: 4, + route: MENU_URLS.guide, + icon: 'setup', + text: 'setup_guide', + order: 4, }, ]; const SETTINGS_ITEMS = [ - { route: SETTINGS_URLS.settings, text: 'general_settings' }, - { route: SETTINGS_URLS.dns, text: 'dns_settings' }, - { route: SETTINGS_URLS.encryption, text: 'encryption_settings' }, - { route: SETTINGS_URLS.clients, text: 'client_settings' }, - { route: SETTINGS_URLS.dhcp, text: 'dhcp_settings' }, + { + route: SETTINGS_URLS.settings, + text: 'general_settings', + }, + { + route: SETTINGS_URLS.dns, + text: 'dns_settings', + }, + { + route: SETTINGS_URLS.encryption, + text: 'encryption_settings', + }, + { + route: SETTINGS_URLS.clients, + text: 'client_settings', + }, + { + route: SETTINGS_URLS.dhcp, + text: 'dhcp_settings', + }, ]; const FILTERS_ITEMS = [ - { route: FILTERS_URLS.dns_blocklists, text: 'dns_blocklists' }, - { route: FILTERS_URLS.dns_allowlists, text: 'dns_allowlists' }, - { route: FILTERS_URLS.dns_rewrites, text: 'dns_rewrites' }, - { route: FILTERS_URLS.blocked_services, text: 'blocked_services' }, - { route: FILTERS_URLS.custom_rules, text: 'custom_filtering_rules' }, + { + route: FILTERS_URLS.dns_blocklists, + text: 'dns_blocklists', + }, + { + route: FILTERS_URLS.dns_allowlists, + text: 'dns_allowlists', + }, + { + route: FILTERS_URLS.dns_rewrites, + text: 'dns_rewrites', + }, + { + route: FILTERS_URLS.blocked_services, + text: 'blocked_services', + }, + { + route: FILTERS_URLS.custom_rules, + text: 'custom_filtering_rules', + }, ]; class Menu extends Component { @@ -52,7 +91,8 @@ class Menu extends Component { getActiveClassForDropdown = (URLS) => { const { pathname } = this.props.location; - const isActivePage = Object.values(URLS).some((item) => item === pathname); + const isActivePage = Object.values(URLS) + .some((item) => item === pathname); return isActivePage ? 'active' : ''; }; @@ -79,18 +119,18 @@ class Menu extends Component { getDropdown = ({ label, order, URLS, icon, ITEMS, }) => ( - - {ITEMS.map((item) => ( - this.getNavLink({ - ...item, - order, - className: 'dropdown-item', - })))} - + + {ITEMS.map((item) => ( + this.getNavLink({ + ...item, + order, + className: 'dropdown-item', + })))} + ); render() { @@ -99,7 +139,7 @@ class Menu extends Component { 'mobile-menu--active': this.props.isMenuOpen, }); return ( - + <>
    {MENU_ITEMS.map((item) => ( @@ -108,26 +148,33 @@ class Menu extends Component { key={item.text} onClick={this.closeMenu} > - {this.getNavLink({ ...item, className: 'nav-link' })} + {this.getNavLink({ + ...item, + className: 'nav-link', + })} ))} - {this.getDropdown({ - order: 1, - label: 'settings', - icon: 'settings', - URLS: SETTINGS_URLS, - ITEMS: SETTINGS_ITEMS, - })} - {this.getDropdown({ - order: 2, - label: 'filters', - icon: 'filters', - URLS: FILTERS_URLS, - ITEMS: FILTERS_ITEMS, - })} +
  • + {this.getDropdown({ + order: 1, + label: 'settings', + icon: 'settings', + URLS: SETTINGS_URLS, + ITEMS: SETTINGS_ITEMS, + })} +
  • +
  • + {this.getDropdown({ + order: 2, + label: 'filters', + icon: 'filters', + URLS: FILTERS_URLS, + ITEMS: FILTERS_ITEMS, + })} +
-
+ ); } } diff --git a/client/src/components/Logs/Cells/getDomainCell.js b/client/src/components/Logs/Cells/getDomainCell.js index c4156920..de142977 100644 --- a/client/src/components/Logs/Cells/getDomainCell.js +++ b/client/src/components/Logs/Cells/getDomainCell.js @@ -21,13 +21,13 @@ const getDomainCell = (props) => { const hasTracker = !!tracker; - const lockIconClass = classNames('icons', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', { + const lockIconClass = classNames('icons icon--small d-none d-sm-block cursor--pointer', { 'icon--active': answer_dnssec, 'icon--disabled': !answer_dnssec, 'my-3': isDetailed, }); - const privacyIconClass = classNames('icons', 'mx-2', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', { + const privacyIconClass = classNames('icons mx-2 icon--small d-none d-sm-block cursor--pointer', { 'icon--active': hasTracker, 'icon--disabled': !hasTracker, 'my-3': isDetailed, @@ -56,7 +56,7 @@ const getDomainCell = (props) => { const renderGrid = (content, idx) => { const preparedContent = typeof content === 'string' ? t(content) : content; - const className = classNames('text-truncate key-colon o-hidden', { + const className = classNames('text-truncate o-hidden', { 'overflow-break': preparedContent.length > 100, }); return
{preparedContent}
; diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index 21e64322..42145c64 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -2,17 +2,20 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; import { useTranslation } from 'react-i18next'; -import debounce from 'lodash/debounce'; -import { useDispatch } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import classNames from 'classnames'; import { DEBOUNCE_FILTER_TIMEOUT, DEFAULT_LOGS_FILTER, FORM_NAME, RESPONSE_FILTER, + RESPONSE_FILTER_QUERIES, } from '../../../helpers/constants'; -import Tooltip from '../../ui/Tooltip'; +import IconTooltip from '../../ui/IconTooltip'; import { setLogsFilter } from '../../../actions/queryLogs'; +import useDebounce from '../../../helpers/useDebounce'; +import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers'; const renderFilterField = ({ input, @@ -25,34 +28,43 @@ const renderFilterField = ({ tooltip, meta: { touched, error }, onClearInputClick, -}) => <> -
- - - -
- -
- - - -
- - + onKeyDown, + normalizeOnBlur, +}) => { + const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur); + + return <> +
+ + + +
+ +
+ + + +
+ + - {!disabled - && touched - && (error && {error})} -; + {!disabled + && touched + && (error && {error})} + ; +}; renderFilterField.propTypes = { input: PropTypes.object.isRequired, @@ -64,65 +76,91 @@ renderFilterField.propTypes = { disabled: PropTypes.string, autoComplete: PropTypes.string, tooltip: PropTypes.string, + onKeyDown: PropTypes.func, + normalizeOnBlur: PropTypes.func, meta: PropTypes.shape({ touched: PropTypes.bool, error: PropTypes.object, }).isRequired, }; +const FORM_NAMES = { + search: 'search', + response_status: 'response_status', +}; + const Form = (props) => { const { className = '', responseStatusClass, - submit, - reset, setIsLoading, + change, } = props; const { t } = useTranslation(); const dispatch = useDispatch(); + const history = useHistory(); - const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT); - const zeroDelaySubmit = () => setTimeout(submit, 0); + const { + response_status, search, + } = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual); - const clearInput = async () => { - await dispatch(setLogsFilter(DEFAULT_LOGS_FILTER)); - await reset(); - }; + const [ + debouncedSearch, + setDebouncedSearch, + ] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT); + + useEffect(() => { + dispatch(setLogsFilter({ + response_status, + search: debouncedSearch, + })); + + history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`); + }, [response_status, debouncedSearch]); + + if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) { + change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]); + } const onInputClear = async () => { setIsLoading(true); - await clearInput(); + setDebouncedSearch(DEFAULT_LOGS_FILTER[FORM_NAMES.search]); + change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]); setIsLoading(false); }; - useEffect(() => clearInput, []); + const onEnterPress = (e) => { + if (e.key === 'Enter') { + setDebouncedSearch(search); + } + }; + + const normalizeOnBlur = (data) => data.trim(); return (
{ e.preventDefault(); - zeroDelaySubmit(); - debouncedSubmit.cancel(); }} >
{Object.values(RESPONSE_FILTER) .map(({ @@ -136,14 +174,13 @@ const Form = (props) => { }; Form.propTypes = { - handleChange: PropTypes.func, className: PropTypes.string, responseStatusClass: PropTypes.string, - submit: PropTypes.func.isRequired, - reset: PropTypes.func.isRequired, + change: PropTypes.func.isRequired, setIsLoading: PropTypes.func.isRequired, }; export default reduxForm({ form: FORM_NAME.LOGS_FILTER, + enableReinitialize: true, })(Form); diff --git a/client/src/components/Logs/Filters/index.js b/client/src/components/Logs/Filters/index.js index 8bb2165c..6484b69a 100644 --- a/client/src/components/Logs/Filters/index.js +++ b/client/src/components/Logs/Filters/index.js @@ -1,20 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Trans } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import Form from './Form'; -import { setLogsFilter } from '../../../actions/queryLogs'; -const Filters = ({ filter, refreshLogs, setIsLoading }) => { - const dispatch = useDispatch(); - - const onSubmit = async (values) => { - setIsLoading(true); - await dispatch(setLogsFilter(values)); - setIsLoading(false); - }; - - return ( +const Filters = ({ filter, refreshLogs, setIsLoading }) => (

query_log @@ -27,17 +16,14 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => { -

+ />
- ); -}; +); Filters.propTypes = { filter: PropTypes.object.isRequired, diff --git a/client/src/components/Logs/Table.js b/client/src/components/Logs/Table.js index 7cc42d0d..131db854 100644 --- a/client/src/components/Logs/Table.js +++ b/client/src/components/Logs/Table.js @@ -49,7 +49,7 @@ const Table = (props) => { isLoading, } = props; - const [t] = useTranslation(); + const { t } = useTranslation(); const toggleBlocking = (type, domain) => { const { @@ -239,7 +239,7 @@ const Table = (props) => { sortable={false} resizable={false} data={logs || []} - loading={isLoading} + loading={isLoading || processingGetLogs} showPageJump={false} showPageSizeOptions={false} onPageChange={changePage} diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 1b4efdcb..b328e34e 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -2,11 +2,14 @@ import React, { Fragment, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Trans } from 'react-i18next'; import Modal from 'react-modal'; -import { useDispatch } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import queryString from 'query-string'; import { - BLOCK_ACTIONS, smallScreenSize, + BLOCK_ACTIONS, TABLE_DEFAULT_PAGE_SIZE, TABLE_FIRST_PAGE, + smallScreenSize, } from '../../helpers/constants'; import Loading from '../ui/Loading'; import Filters from './Filters'; @@ -15,13 +18,15 @@ import Disabled from './Disabled'; import { getFilteringStatus } from '../../actions/filtering'; import { getClients } from '../../actions'; import { getDnsConfig } from '../../actions/dnsConfig'; -import { getLogsConfig } from '../../actions/queryLogs'; +import { + getLogsConfig, + refreshFilteredLogs, + resetFilteredLogs, + setFilteredLogs, +} from '../../actions/queryLogs'; import { addSuccessToast } from '../../actions/toasts'; import './Logs.css'; -const INITIAL_REQUEST = true; -const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST]; - export const processContent = (data, buttonType) => Object.entries(data) .map(([key, value]) => { if (!value) { @@ -56,22 +61,44 @@ export const processContent = (data, buttonType) => Object.entries(data) const Logs = (props) => { const dispatch = useDispatch(); + const history = useHistory(); + + const { + response_status: response_status_url_param = '', + search: search_url_param = '', + } = queryString.parse(history.location.search); + + const { filter } = useSelector((state) => state.queryLogs, shallowEqual); + + const search = filter?.search || search_url_param; + const response_status = filter?.response_status || response_status_url_param; + const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize); const [detailedDataCurrent, setDetailedDataCurrent] = useState({}); const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK); const [isModalOpened, setModalOpened] = useState(false); const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + (async () => { + setIsLoading(true); + await dispatch(setFilteredLogs({ + search, + response_status, + })); + setIsLoading(false); + })(); + }, [response_status, search]); + const { filtering, setLogsPage, setLogsPagination, - setLogsFilter, toggleDetailedLogs, dashboard, dnsConfig, queryLogs: { - filter, enabled, processingGetConfig, processingAdditionalLogs, @@ -92,16 +119,10 @@ const Logs = (props) => { } }; - useEffect(() => { - mediaQuery.addListener(mediaQueryHandler); - - return () => mediaQuery.removeListener(mediaQueryHandler); - }, []); - const closeModal = () => setModalOpened(false); const getLogs = (older_than, page, initial) => { - if (props.queryLogs.enabled) { + if (enabled) { props.getLogs({ older_than, page, @@ -112,6 +133,8 @@ const Logs = (props) => { }; useEffect(() => { + mediaQuery.addEventListener('change', mediaQueryHandler); + (async () => { setIsLoading(true); dispatch(setLogsPage(TABLE_FIRST_PAGE)); @@ -119,7 +142,6 @@ const Logs = (props) => { dispatch(getClients()); try { await Promise.all([ - getLogs(...INITIAL_REQUEST_DATA), dispatch(getLogsConfig()), dispatch(getDnsConfig()), ]); @@ -129,13 +151,18 @@ const Logs = (props) => { setIsLoading(false); } })(); + + return () => { + mediaQuery.removeEventListener('change', mediaQueryHandler); + dispatch(resetFilteredLogs()); + }; }, []); const refreshLogs = async () => { setIsLoading(true); await Promise.all([ dispatch(setLogsPage(TABLE_FIRST_PAGE)), - getLogs(...INITIAL_REQUEST_DATA), + dispatch(refreshFilteredLogs()), ]); dispatch(addSuccessToast('query_log_updated')); setIsLoading(false); @@ -145,13 +172,15 @@ const Logs = (props) => { <> {enabled && processingGetConfig && } {enabled && !processingGetConfig && ( - + <> { {processContent(detailedDataCurrent, buttonType)} - + )} {!enabled && !processingGetConfig && ( @@ -219,7 +248,6 @@ Logs.propTypes = { setRules: PropTypes.func.isRequired, addSuccessToast: PropTypes.func.isRequired, setLogsPagination: PropTypes.func.isRequired, - setLogsFilter: PropTypes.func.isRequired, setLogsPage: PropTypes.func.isRequired, toggleDetailedLogs: PropTypes.func.isRequired, dnsConfig: PropTypes.object.isRequired, diff --git a/client/src/components/Settings/Clients/AutoClients.js b/client/src/components/Settings/Clients/AutoClients.js index 90995f31..1fd8224e 100644 --- a/client/src/components/Settings/Clients/AutoClients.js +++ b/client/src/components/Settings/Clients/AutoClients.js @@ -7,6 +7,7 @@ import Card from '../../ui/Card'; import CellWrap from '../../ui/CellWrap'; import whoisCell from './whoisCell'; +import LogsSearchLink from '../../ui/LogsSearchLink'; const COLUMN_MIN_WIDTH = 200; @@ -49,7 +50,9 @@ class AutoClients extends Component { return (
- {clientStats} + + {clientStats} +
); diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index 2fda1bd6..e767cfb9 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -8,6 +8,7 @@ import { normalizeTextarea } from '../../../helpers/helpers'; import Card from '../../ui/Card'; import Modal from './Modal'; import CellWrap from '../../ui/CellWrap'; +import LogsSearchLink from '../../ui/LogsSearchLink'; class ClientsTable extends Component { handleFormAdd = (values) => { @@ -49,7 +50,10 @@ class ClientsTable extends Component { }; getOptionsWithLabels = (options) => ( - options.map((option) => ({ value: option, label: option })) + options.map((option) => ({ + value: option, + label: option, + })) ); getClient = (name, clients) => { @@ -203,7 +207,15 @@ class ClientsTable extends Component { accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0, sortMethod: (a, b) => b - a, minWidth: 120, - Cell: CellWrap, + Cell: (row) => { + const content = CellWrap(row); + + if (!row.value) { + return content; + } + + return {content}; + }, }, { Header: this.props.t('actions_table_header'), @@ -311,7 +323,6 @@ class ClientsTable extends Component { > client_add - ( -
-
- {formatNumber(value)} - {percent}% -
-
-
-
+const Cell = ({ + value, percent, color, search, +}) =>
+
+ {formatNumber(value)} + {percent}%
-); +
+
+
+
; Cell.propTypes = { value: PropTypes.number.isRequired, percent: PropTypes.number.isRequired, color: PropTypes.string.isRequired, + search: PropTypes.string, + onSearchRedirect: PropTypes.func, }; export default Cell; diff --git a/client/src/components/ui/Tooltip.css b/client/src/components/ui/IconTooltip.css similarity index 100% rename from client/src/components/ui/Tooltip.css rename to client/src/components/ui/IconTooltip.css diff --git a/client/src/components/ui/IconTooltip.js b/client/src/components/ui/IconTooltip.js new file mode 100644 index 00000000..7a7af2fa --- /dev/null +++ b/client/src/components/ui/IconTooltip.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './IconTooltip.css'; +import { useTranslation } from 'react-i18next'; + +const IconTooltip = ({ text, type = '' }) => { + const { t } = useTranslation(); + + return
; +}; + +IconTooltip.propTypes = { + text: PropTypes.string.isRequired, + type: PropTypes.string, +}; + +export default IconTooltip; diff --git a/client/src/components/ui/LogsSearchLink.css b/client/src/components/ui/LogsSearchLink.css new file mode 100644 index 00000000..1649e1af --- /dev/null +++ b/client/src/components/ui/LogsSearchLink.css @@ -0,0 +1,7 @@ +.stats__link { + color: inherit; +} + +.stats__link:hover { + cursor: pointer; +} diff --git a/client/src/components/ui/LogsSearchLink.js b/client/src/components/ui/LogsSearchLink.js new file mode 100644 index 00000000..692f78d2 --- /dev/null +++ b/client/src/components/ui/LogsSearchLink.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import './LogsSearchLink.css'; +import { getLogsUrlParams } from '../../helpers/helpers'; +import { MENU_URLS } from '../../helpers/constants'; + +const LogsSearchLink = ({ + search = '', response_status = '', children, link = MENU_URLS.logs, +}) => { + const { t } = useTranslation(); + + const to = link === MENU_URLS.logs ? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}` : link; + + return {children}; +}; + +LogsSearchLink.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.element]).isRequired, + search: PropTypes.string, + response_status: PropTypes.string, + link: PropTypes.string, +}; + +export default LogsSearchLink; diff --git a/client/src/components/ui/Tooltip.js b/client/src/components/ui/Tooltip.js deleted file mode 100644 index 90bd69e1..00000000 --- a/client/src/components/ui/Tooltip.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import './Tooltip.css'; - -const Tooltip = ({ text, type = '' }) =>
; - -Tooltip.propTypes = { - text: PropTypes.string.isRequired, - type: PropTypes.string, -}; - -export default Tooltip; diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js index f25aab3b..92858cbf 100644 --- a/client/src/containers/Logs.js +++ b/client/src/containers/Logs.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { getFilteringStatus, setRules } from '../actions/filtering'; import { - getLogs, setLogsPagination, setLogsFilter, setLogsPage, toggleDetailedLogs, + getLogs, setLogsPagination, setLogsPage, toggleDetailedLogs, } from '../actions/queryLogs'; import Logs from '../components/Logs'; import { addSuccessToast } from '../actions/toasts'; @@ -26,7 +26,6 @@ const mapDispatchToProps = { setRules, addSuccessToast, setLogsPagination, - setLogsFilter, setLogsPage, toggleDetailedLogs, }; diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index ae77faa5..e96086c5 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -397,6 +397,11 @@ export const RESPONSE_FILTER = { }, }; +export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER).reduce((acc, { query }) => { + acc[query] = query; + return acc; +}, {}); + export const FILTERED_STATUS_TO_META_MAP = { [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: { label: RESPONSE_FILTER.ALLOWED.label, diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 3507e4ab..e075a3c5 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -11,6 +11,7 @@ import axios from 'axios'; import i18n from 'i18next'; import uniqBy from 'lodash/uniqBy'; import ipaddr from 'ipaddr.js'; +import queryString from 'query-string'; import versionCompare from './versionCompare'; import { getTrackerData } from './trackers/trackers'; @@ -618,6 +619,16 @@ export const selectCompletedFields = (values) => Object.entries(values) return acc; }, {}); +/** + * @param {string} search + * @param {string} [response_status] + * @returns {string} + */ +export const getLogsUrlParams = (search, response_status) => `?${queryString.stringify({ + search, + response_status, +})}`; + export const processContent = (content) => (Array.isArray(content) ? content.filter(([, value]) => value) diff --git a/client/src/helpers/useDebounce.js b/client/src/helpers/useDebounce.js new file mode 100644 index 00000000..32359f23 --- /dev/null +++ b/client/src/helpers/useDebounce.js @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react'; + +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect( + () => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, + [value, delay], + ); + + return [debouncedValue, setDebouncedValue]; +}; + +export default useDebounce; diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index bff4896d..d1e24ac3 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -25,14 +25,14 @@ const queryLogs = handleActions( page: payload, }), - [actions.setLogsFilterRequest]: (state) => ({ ...state, processingGetLogs: true }), - [actions.setLogsFilterFailure]: (state) => ({ ...state, processingGetLogs: false }), + [actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }), + [actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.toggleDetailedLogs]: (state, { payload }) => ({ ...state, isDetailed: payload, }), - [actions.setLogsFilterSuccess]: (state, { payload }) => { + [actions.setFilteredLogsSuccess]: (state, { payload }) => { const { logs, oldest, filter } = payload; const pageSize = TABLE_DEFAULT_PAGE_SIZE; const page = 0; @@ -57,6 +57,12 @@ const queryLogs = handleActions( }; }, + [actions.setLogsFilterRequest]: (state, { payload }) => { + const { filter } = payload; + + return { ...state, filter }; + }, + [actions.getLogsRequest]: (state) => ({ ...state, processingGetLogs: true }), [actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.getLogsSuccess]: (state, { payload }) => {