+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: d4c811f9 8a417604
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 13 15:30:21 2020 +0300

    Merge branch 'master' into feature/1625

commit d4c811f9630dee448012434e2f50f34ab8b8b899
Merge: b0a037da a33164bf
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:
Artem Baskal 2020-07-13 16:06:56 +03:00
parent 8a417604a9
commit da4a1ec23d
30 changed files with 591 additions and 331 deletions

View File

@ -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
View File

@ -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
View File

@ -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",

View File

@ -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"
}

View File

@ -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');

View File

@ -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} />

View File

@ -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 = ({

View File

@ -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) => {

View File

@ -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;

View File

@ -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 = ({

View File

@ -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>

View File

@ -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>
</>
);
}
}

View File

@ -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>;

View File

@ -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);

View File

@ -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,

View File

@ -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}

View File

@ -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,

View File

@ -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>
);

View File

@ -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}

View File

@ -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;

View 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;

View File

@ -0,0 +1,7 @@
.stats__link {
color: inherit;
}
.stats__link:hover {
cursor: pointer;
}

View 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;

View File

@ -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;

View File

@ -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,
};

View File

@ -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,

View File

@ -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)

View 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;

View File

@ -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 }) => {