+client: "Drill down" to activity reports
Close #1625 Squashed commit of the following: commit a01f12c4e5831c43dbe3ae8a80f4db12077dbb2a Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 15:50:15 2020 +0300 minor commit b8ceb17a3b12e47de81af85fa30c2961a4a42fab Merge: 702c55ed fecf5494 Author: Andrey Meshkov <am@adguard.com> 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 <am@adguard.com> Date: Mon Jul 13 15:32:41 2020 +0300 fix makefile when there's no gopath commit fecf5494b8c1719cb70044f336fe99c341802d25 Merge: d4c811f98a417604
Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 15:30:21 2020 +0300 Merge branch 'master' into feature/1625 commit d4c811f9630dee448012434e2f50f34ab8b8b899 Merge: b0a037daa33164bf
Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 12:35:16 2020 +0300 Merge branch 'master' into feature/1625 commit b0a037daf48913fd8a4cda16d520835630072520 Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 12:34:42 2020 +0300 Simplify sync logs action creators commit eeeb620ae100a554f59783fc2a14fad525ce1a82 Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 11:17:08 2020 +0300 Review changes commit 4cbc59eec5c794df18d6cb9b33f39091ce7cfde9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 15:23:37 2020 +0300 Update tracker tooltip class commit 0a705301d4726af1c8f7f7a5776b11d338ab1d54 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 13:46:10 2020 +0300 Replace depricated addListener commit 2ac0843239853da1725d2e038b5e4cbaef253732 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 13:39:45 2020 +0300 Validate response_status url param commit 2178039ebbd0cbe2c0048cb5ab7ad7c7e7571bd1 Author: ArtemBaskal <a.baskal@adguard.com> 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 <a.baskal@adguard.com> Date: Thu Jul 9 19:41:48 2020 +0300 Normalize input search commit 3fded3575b21bdd017723f5e487c268074599e4f Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 18:20:05 2020 +0300 Optimize search commit 9073e032e4aadcdef9d826f16a10c300ee46b30e Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 14:28:41 2020 +0300 Update url string params commit a18cffc8bfac83103fb78ffae2f786f89aea8ba1 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 12:55:50 2020 +0300 Fix reset search commit 33f769aed56369aacedd29ffd52b527b527d4a59 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jul 8 19:13:21 2020 +0300 WIP: Add permlinks commit 4422641cf5cff06c8485ea23d58e5d42f7cca5cd Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jul 8 14:42:28 2020 +0300 Refactor Counters, add response_status links to query log commit e8bb0b70ca55f31ef3fcdda13dcaad6f5d8479b5 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 19:33:04 2020 +0300 Delete unnecessary file commit b20816e9dad79866e3ec04d3093c972967b3b226 Merge: 6281084e d2c3af5c Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 19:30:44 2020 +0300 Resolve conflict commit d2c3af5cf227d76f876d6d94ca016d4b242b2515 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 17:14:51 2020 +0300 + client: Add git hooks ... and 5 more commits
This commit is contained in:
parent
8a417604a9
commit
da4a1ec23d
2
Makefile
2
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
|
||||
|
||||
|
67
client/package-lock.json
generated
vendored
67
client/package-lock.json
generated
vendored
@ -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",
|
||||
|
1
client/package.json
vendored
1
client/package.json
vendored
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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 && (
|
||||
<>
|
||||
<Route path={MENU_URLS.root} exact component={Dashboard} />
|
||||
<Route path={MENU_URLS.logs} component={Logs} />
|
||||
<Route
|
||||
path={[`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs]}
|
||||
component={Logs} />
|
||||
<Route path={MENU_URLS.guide} component={SetupGuide} />
|
||||
<Route path={SETTINGS_URLS.settings} component={Settings} />
|
||||
<Route path={SETTINGS_URLS.dns} component={Dns} />
|
||||
|
@ -14,7 +14,11 @@ const CountCell = (totalBlocked) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(totalBlocked, value);
|
||||
|
||||
return <Cell value={value} percent={percent} color={STATUS_COLORS.red} />;
|
||||
return <Cell value={value}
|
||||
percent={percent}
|
||||
color={STATUS_COLORS.red}
|
||||
search={row.original.domain}
|
||||
/>;
|
||||
};
|
||||
|
||||
const BlockedDomains = ({
|
||||
|
@ -25,7 +25,7 @@ const countCell = (dnsQueries) => function cell(row) {
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const percentColor = getClientsPercentColor(percent);
|
||||
|
||||
return <Cell value={value} percent={percent} color={percentColor} />;
|
||||
return <Cell value={value} percent={percent} color={percentColor} search={row.original.ip} />;
|
||||
};
|
||||
|
||||
const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => {
|
||||
|
@ -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
|
||||
? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
|
||||
: count;
|
||||
|
||||
const Counters = (props) => {
|
||||
return <tr key={label}>
|
||||
<td>
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
<IconTooltip text={tooltipTitle} type="tooltip-custom--narrow" />
|
||||
</td>
|
||||
<td className="text-right"><strong>{content}</strong></td>
|
||||
</tr>;
|
||||
};
|
||||
|
||||
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: [<a href="#filters" key="0">link</a>],
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Card
|
||||
@ -35,104 +84,23 @@ const Counters = (props) => {
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<table className="table card-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>dns_query</Trans>
|
||||
<Tooltip text={tooltipTitle} type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(dnsQueries)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans components={[<a href="#filters" key="0">link</a>]}>
|
||||
blocked_by
|
||||
</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(blockedFiltering)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>stats_malware_phishing</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours_by_sec')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedSafebrowsing)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>stats_adult</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours_adult')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedParental)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>enforced_save_search</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_to_safe_search')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedSafesearch)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>average_processing_time</Trans>
|
||||
<Tooltip text={t('average_processing_time_hint')} type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>{rows.map(Row)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -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 <Cell value={value} percent={percent} color={percentColor} />;
|
||||
return <Cell value={value} percent={percent} color={percentColor}
|
||||
search={row.original.domain} />;
|
||||
};
|
||||
|
||||
const QueriedDomains = ({
|
||||
|
@ -111,13 +111,6 @@ class Dashboard extends Component {
|
||||
<div className="col-lg-6">
|
||||
<Counters
|
||||
subtitle={subtitle}
|
||||
interval={stats.interval}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
blockedFiltering={stats.numBlockedFiltering}
|
||||
replacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
replacedParental={stats.numReplacedParental}
|
||||
replacedSafesearch={stats.numReplacedSafesearch}
|
||||
avgProcessingTime={stats.avgProcessingTime}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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,
|
||||
}) => (
|
||||
<Dropdown
|
||||
label={this.props.t(label)}
|
||||
baseClassName={`dropdown nav-item order-${order}`}
|
||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||
icon={icon}>
|
||||
{ITEMS.map((item) => (
|
||||
this.getNavLink({
|
||||
...item,
|
||||
order,
|
||||
className: 'dropdown-item',
|
||||
})))}
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
label={this.props.t(label)}
|
||||
baseClassName='dropdown'
|
||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||
icon={icon}>
|
||||
{ITEMS.map((item) => (
|
||||
this.getNavLink({
|
||||
...item,
|
||||
order,
|
||||
className: 'dropdown-item',
|
||||
})))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
render() {
|
||||
@ -99,7 +139,7 @@ class Menu extends Component {
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
{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',
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
{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,
|
||||
})}
|
||||
<li className="nav-item order-1">
|
||||
{this.getDropdown({
|
||||
order: 1,
|
||||
label: 'settings',
|
||||
icon: 'settings',
|
||||
URLS: SETTINGS_URLS,
|
||||
ITEMS: SETTINGS_ITEMS,
|
||||
})}
|
||||
</li>
|
||||
<li className="nav-item order-2">
|
||||
{this.getDropdown({
|
||||
order: 2,
|
||||
label: 'filters',
|
||||
icon: 'filters',
|
||||
URLS: FILTERS_URLS,
|
||||
ITEMS: FILTERS_ITEMS,
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 <div key={idx} className={className}>{preparedContent}</div>;
|
||||
|
@ -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,
|
||||
}) => <>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder} />
|
||||
<div
|
||||
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
|
||||
<svg className="icons icon--smallest icon--gray" onClick={onClearInputClick}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="input-group-search input-group-search__icon--tooltip">
|
||||
<Tooltip text={tooltip} type='tooltip-custom--logs' />
|
||||
onKeyDown,
|
||||
normalizeOnBlur,
|
||||
}) => {
|
||||
const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur);
|
||||
|
||||
return <>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<div
|
||||
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
|
||||
<svg className="icons icon--smallest icon--gray" onClick={onClearInputClick}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="input-group-search input-group-search__icon--tooltip">
|
||||
<IconTooltip text={tooltip} type='tooltip-custom--logs' />
|
||||
</span>
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<form className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
zeroDelaySubmit();
|
||||
debouncedSubmit.cancel();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
id="search"
|
||||
name="search"
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control--search form-control--transparent', className)}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onChange={debouncedSubmit}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
/>
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name="response_status"
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
|
||||
onChange={zeroDelaySubmit}
|
||||
>
|
||||
{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);
|
||||
|
@ -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 }) => (
|
||||
<div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
<Trans>query_log</Trans>
|
||||
@ -27,17 +16,14 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
<use xlinkHref="#update" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</h1>
|
||||
<Form
|
||||
responseStatusClass="d-sm-block"
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
|
@ -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}
|
||||
|
@ -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 && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Filters
|
||||
filter={filter}
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
setLogsFilter={setLogsFilter}
|
||||
refreshLogs={refreshLogs}
|
||||
/>
|
||||
<Table
|
||||
@ -201,7 +230,7 @@ const Logs = (props) => {
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<Disabled />
|
||||
@ -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,
|
||||
|
@ -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 (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
<LogsSearchLink search={row.original.ip}>
|
||||
{clientStats}
|
||||
</LogsSearchLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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 <LogsSearchLink search={row.original.ids[0]}>{content}</LogsSearchLink>;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('actions_table_header'),
|
||||
@ -311,7 +323,6 @@ class ClientsTable extends Component {
|
||||
>
|
||||
<Trans>client_add</Trans>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
modalType={modalType}
|
||||
|
@ -1,30 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import LogsSearchLink from './LogsSearchLink';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
|
||||
const Cell = ({ value, percent, color }) => (
|
||||
<div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong>{formatNumber(value)}</strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
</div>
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const Cell = ({
|
||||
value, percent, color, search,
|
||||
}) => <div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong><LogsSearchLink search={search}>{formatNumber(value)}</LogsSearchLink></strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
</div>
|
||||
);
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
percent: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
search: PropTypes.string,
|
||||
onSearchRedirect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Cell;
|
||||
|
19
client/src/components/ui/IconTooltip.js
Normal file
19
client/src/components/ui/IconTooltip.js
Normal file
@ -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 <div data-tooltip={t(text)}
|
||||
className={`tooltip-custom ml-1 ${type}`} />;
|
||||
};
|
||||
|
||||
IconTooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default IconTooltip;
|
7
client/src/components/ui/LogsSearchLink.css
Normal file
7
client/src/components/ui/LogsSearchLink.css
Normal file
@ -0,0 +1,7 @@
|
||||
.stats__link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stats__link:hover {
|
||||
cursor: pointer;
|
||||
}
|
33
client/src/components/ui/LogsSearchLink.js
Normal file
33
client/src/components/ui/LogsSearchLink.js
Normal file
@ -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 <Link to={to}
|
||||
className={'stats__link'}
|
||||
tabIndex={0}
|
||||
title={t('click_to_view_queries')}
|
||||
aria-label={t('click_to_view_queries')}>{children}</Link>;
|
||||
};
|
||||
|
||||
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;
|
@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Tooltip.css';
|
||||
|
||||
const Tooltip = ({ text, type = '' }) => <div data-tooltip={text}
|
||||
className={`tooltip-custom ml-1 ${type}`} />;
|
||||
|
||||
Tooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
22
client/src/helpers/useDebounce.js
Normal file
22
client/src/helpers/useDebounce.js
Normal file
@ -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;
|
@ -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 }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user