+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
|
all: build
|
||||||
|
|
||||||
build: dependencies client
|
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)"
|
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
|
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||||
|
|
||||||
|
|
|
@ -1909,6 +1909,11 @@
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true
|
"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": {
|
"micromatch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
||||||
|
@ -3097,6 +3102,11 @@
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||||
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||||
|
@ -4818,8 +4828,7 @@
|
||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"deep-equal": {
|
"deep-equal": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
@ -11044,6 +11053,24 @@
|
||||||
"prepend-http": "^1.0.0",
|
"prepend-http": "^1.0.0",
|
||||||
"query-string": "^4.1.0",
|
"query-string": "^4.1.0",
|
||||||
"sort-keys": "^1.0.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": {
|
"npm-run-path": {
|
||||||
|
@ -12138,13 +12165,13 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"query-string": {
|
"query-string": {
|
||||||
"version": "4.3.4",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.1.tgz",
|
||||||
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
|
"integrity": "sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"object-assign": "^4.1.0",
|
"decode-uri-component": "^0.2.0",
|
||||||
"strict-uri-encode": "^1.0.0"
|
"split-on-first": "^1.0.0",
|
||||||
|
"strict-uri-encode": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"querystring": {
|
"querystring": {
|
||||||
|
@ -13455,6 +13482,18 @@
|
||||||
"is-data-descriptor": "^1.0.0",
|
"is-data-descriptor": "^1.0.0",
|
||||||
"kind-of": "^6.0.2"
|
"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==",
|
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
|
||||||
"dev": true
|
"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": {
|
"split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||||
|
@ -13833,10 +13877,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strict-uri-encode": {
|
"strict-uri-encode": {
|
||||||
"version": "1.1.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
|
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"string-length": {
|
"string-length": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"nanoid": "^3.1.9",
|
"nanoid": "^3.1.9",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
|
"query-string": "^6.13.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-click-outside": "^3.0.1",
|
"react-click-outside": "^3.0.1",
|
||||||
"react-dom": "^16.13.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_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_regional_desc": "Lists that focus on regional ads and tracking servers",
|
||||||
"filter_category_other_desc": "Other blocklists",
|
"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 apiClient from '../api/Api';
|
||||||
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
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';
|
import { addErrorToast, addSuccessToast } from './toasts';
|
||||||
|
|
||||||
const getLogsWithParams = async (config) => {
|
const getLogsWithParams = async (config) => {
|
||||||
const { older_than, filter, ...values } = 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;
|
const { data, oldest } = rawLogs;
|
||||||
let logs = normalizeLogs(data);
|
let logs = normalizeLogs(data);
|
||||||
const clientsParams = getParamsForClientsSearch(logs, 'client');
|
const clientsParams = getParamsForClientsSearch(logs, 'client');
|
||||||
|
@ -18,7 +25,11 @@ const getLogsWithParams = async (config) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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());
|
dispatch(getAdditionalLogsRequest());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const additionalLogs = await getLogsWithParams({ older_than: oldest, filter });
|
const additionalLogs = await getLogsWithParams({
|
||||||
|
older_than: oldest,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
if (additionalLogs.oldest.length > 0) {
|
if (additionalLogs.oldest.length > 0) {
|
||||||
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
|
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
|
||||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||||
|
@ -69,13 +83,19 @@ export const getLogs = (config) => async (dispatch, getState) => {
|
||||||
dispatch(getLogsRequest());
|
dispatch(getLogsRequest());
|
||||||
try {
|
try {
|
||||||
const { isFiltered, filter, page } = getState().queryLogs;
|
const { isFiltered, filter, page } = getState().queryLogs;
|
||||||
const data = await getLogsWithParams({ ...config, filter });
|
const data = await getLogsWithParams({
|
||||||
|
...config,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
|
||||||
if (isFiltered) {
|
if (isFiltered) {
|
||||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
dispatch(getLogsSuccess(updatedData));
|
dispatch(getLogsSuccess(updatedData));
|
||||||
dispatch(setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE }));
|
dispatch(setLogsPagination({
|
||||||
|
page,
|
||||||
|
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
dispatch(getLogsSuccess(data));
|
dispatch(getLogsSuccess(data));
|
||||||
}
|
}
|
||||||
|
@ -86,24 +106,48 @@ export const getLogs = (config) => async (dispatch, getState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
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 {
|
try {
|
||||||
const data = await getLogsWithParams({ older_than: '', filter });
|
const data = await getLogsWithParams({
|
||||||
|
older_than: '',
|
||||||
|
filter,
|
||||||
|
});
|
||||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
|
|
||||||
dispatch(setLogsFilterSuccess({ ...updatedData, filter }));
|
dispatch(setFilteredLogsSuccess({
|
||||||
|
...updatedData,
|
||||||
|
filter,
|
||||||
|
}));
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ 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 clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');
|
||||||
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
|
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
|
||||||
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');
|
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');
|
||||||
|
|
|
@ -36,7 +36,7 @@ import i18n from '../../i18n';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
||||||
import Services from '../Filters/Services';
|
import Services from '../Filters/Services';
|
||||||
import { setHtmlLangAttr } from '../../helpers/helpers';
|
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -111,7 +111,9 @@ class App extends Component {
|
||||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||||
<>
|
<>
|
||||||
<Route path={MENU_URLS.root} exact component={Dashboard} />
|
<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={MENU_URLS.guide} component={SetupGuide} />
|
||||||
<Route path={SETTINGS_URLS.settings} component={Settings} />
|
<Route path={SETTINGS_URLS.settings} component={Settings} />
|
||||||
<Route path={SETTINGS_URLS.dns} component={Dns} />
|
<Route path={SETTINGS_URLS.dns} component={Dns} />
|
||||||
|
|
|
@ -14,7 +14,11 @@ const CountCell = (totalBlocked) => function cell(row) {
|
||||||
const { value } = row;
|
const { value } = row;
|
||||||
const percent = getPercent(totalBlocked, value);
|
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 = ({
|
const BlockedDomains = ({
|
||||||
|
|
|
@ -25,7 +25,7 @@ const countCell = (dnsQueries) => function cell(row) {
|
||||||
const percent = getPercent(dnsQueries, value);
|
const percent = getPercent(dnsQueries, value);
|
||||||
const percentColor = getClientsPercentColor(percent);
|
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) => {
|
const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => {
|
||||||
|
|
|
@ -1,31 +1,80 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import propTypes from 'prop-types';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import round from 'lodash/round';
|
import round from 'lodash/round';
|
||||||
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
import Tooltip from '../ui/Tooltip';
|
import IconTooltip from '../ui/IconTooltip';
|
||||||
import { formatNumber } from '../../helpers/helpers';
|
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 {
|
const {
|
||||||
t,
|
|
||||||
interval,
|
interval,
|
||||||
refreshButton,
|
numDnsQueries,
|
||||||
subtitle,
|
numBlockedFiltering,
|
||||||
dnsQueries,
|
numReplacedSafebrowsing,
|
||||||
blockedFiltering,
|
numReplacedParental,
|
||||||
replacedSafebrowsing,
|
numReplacedSafesearch,
|
||||||
replacedParental,
|
|
||||||
replacedSafesearch,
|
|
||||||
avgProcessingTime,
|
avgProcessingTime,
|
||||||
} = props;
|
} = useSelector((state) => state.stats, shallowEqual);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const tooltipTitle = interval === 1
|
const rows = [
|
||||||
? t('number_of_dns_query_24_hours')
|
{
|
||||||
: t('number_of_dns_query_days', { count: interval });
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -35,104 +84,23 @@ const Counters = (props) => {
|
||||||
refresh={refreshButton}
|
refresh={refreshButton}
|
||||||
>
|
>
|
||||||
<table className="table card-table">
|
<table className="table card-table">
|
||||||
<tbody>
|
<tbody>{rows.map(Row)}</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>
|
|
||||||
</table>
|
</table>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Counters.propTypes = {
|
Row.propTypes = {
|
||||||
dnsQueries: PropTypes.number.isRequired,
|
label: propTypes.string.isRequired,
|
||||||
blockedFiltering: PropTypes.number.isRequired,
|
count: propTypes.string.isRequired,
|
||||||
replacedSafebrowsing: PropTypes.number.isRequired,
|
response_status: propTypes.string,
|
||||||
replacedParental: PropTypes.number.isRequired,
|
tooltipTitle: propTypes.string.isRequired,
|
||||||
replacedSafesearch: PropTypes.number.isRequired,
|
translationComponents: propTypes.arrayOf(propTypes.element),
|
||||||
avgProcessingTime: PropTypes.number.isRequired,
|
|
||||||
refreshButton: PropTypes.node.isRequired,
|
|
||||||
subtitle: PropTypes.string.isRequired,
|
|
||||||
interval: PropTypes.number.isRequired,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) => {
|
const getQueriedPercentColor = (percent) => {
|
||||||
if (percent > 10) {
|
if (percent > 10) {
|
||||||
return STATUS_COLORS.red;
|
return STATUS_COLORS.red;
|
||||||
} if (percent > 5) {
|
}
|
||||||
|
if (percent > 5) {
|
||||||
return STATUS_COLORS.yellow;
|
return STATUS_COLORS.yellow;
|
||||||
}
|
}
|
||||||
return STATUS_COLORS.green;
|
return STATUS_COLORS.green;
|
||||||
|
@ -24,7 +25,8 @@ const countCell = (dnsQueries) => function cell(row) {
|
||||||
const percent = getPercent(dnsQueries, value);
|
const percent = getPercent(dnsQueries, value);
|
||||||
const percentColor = getQueriedPercentColor(percent);
|
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 = ({
|
const QueriedDomains = ({
|
||||||
|
|
|
@ -111,13 +111,6 @@ class Dashboard extends Component {
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<Counters
|
<Counters
|
||||||
subtitle={subtitle}
|
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}
|
refreshButton={refreshButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import enhanceWithClickOutside from 'react-click-outside';
|
import enhanceWithClickOutside from 'react-click-outside';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
|
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
|
||||||
import Dropdown from '../ui/Dropdown';
|
import Dropdown from '../ui/Dropdown';
|
||||||
|
|
||||||
const MENU_ITEMS = [
|
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
|
// Settings dropdown should have visual order 1
|
||||||
|
@ -18,27 +21,63 @@ const MENU_ITEMS = [
|
||||||
// Filters dropdown should have visual order 2
|
// 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 = [
|
const SETTINGS_ITEMS = [
|
||||||
{ route: SETTINGS_URLS.settings, text: 'general_settings' },
|
{
|
||||||
{ route: SETTINGS_URLS.dns, text: 'dns_settings' },
|
route: SETTINGS_URLS.settings,
|
||||||
{ route: SETTINGS_URLS.encryption, text: 'encryption_settings' },
|
text: 'general_settings',
|
||||||
{ route: SETTINGS_URLS.clients, text: 'client_settings' },
|
},
|
||||||
{ route: SETTINGS_URLS.dhcp, text: 'dhcp_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 = [
|
const FILTERS_ITEMS = [
|
||||||
{ route: FILTERS_URLS.dns_blocklists, text: 'dns_blocklists' },
|
{
|
||||||
{ route: FILTERS_URLS.dns_allowlists, text: 'dns_allowlists' },
|
route: FILTERS_URLS.dns_blocklists,
|
||||||
{ route: FILTERS_URLS.dns_rewrites, text: 'dns_rewrites' },
|
text: 'dns_blocklists',
|
||||||
{ route: FILTERS_URLS.blocked_services, text: 'blocked_services' },
|
},
|
||||||
{ route: FILTERS_URLS.custom_rules, text: 'custom_filtering_rules' },
|
{
|
||||||
|
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 {
|
class Menu extends Component {
|
||||||
|
@ -52,7 +91,8 @@ class Menu extends Component {
|
||||||
|
|
||||||
getActiveClassForDropdown = (URLS) => {
|
getActiveClassForDropdown = (URLS) => {
|
||||||
const { pathname } = this.props.location;
|
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' : '';
|
return isActivePage ? 'active' : '';
|
||||||
};
|
};
|
||||||
|
@ -81,7 +121,7 @@ class Menu extends Component {
|
||||||
}) => (
|
}) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label={this.props.t(label)}
|
label={this.props.t(label)}
|
||||||
baseClassName={`dropdown nav-item order-${order}`}
|
baseClassName='dropdown'
|
||||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||||
icon={icon}>
|
icon={icon}>
|
||||||
{ITEMS.map((item) => (
|
{ITEMS.map((item) => (
|
||||||
|
@ -99,7 +139,7 @@ class Menu extends Component {
|
||||||
'mobile-menu--active': this.props.isMenuOpen,
|
'mobile-menu--active': this.props.isMenuOpen,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<div className={menuClass}>
|
<div className={menuClass}>
|
||||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||||
{MENU_ITEMS.map((item) => (
|
{MENU_ITEMS.map((item) => (
|
||||||
|
@ -108,9 +148,13 @@ class Menu extends Component {
|
||||||
key={item.text}
|
key={item.text}
|
||||||
onClick={this.closeMenu}
|
onClick={this.closeMenu}
|
||||||
>
|
>
|
||||||
{this.getNavLink({ ...item, className: 'nav-link' })}
|
{this.getNavLink({
|
||||||
|
...item,
|
||||||
|
className: 'nav-link',
|
||||||
|
})}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<li className="nav-item order-1">
|
||||||
{this.getDropdown({
|
{this.getDropdown({
|
||||||
order: 1,
|
order: 1,
|
||||||
label: 'settings',
|
label: 'settings',
|
||||||
|
@ -118,6 +162,8 @@ class Menu extends Component {
|
||||||
URLS: SETTINGS_URLS,
|
URLS: SETTINGS_URLS,
|
||||||
ITEMS: SETTINGS_ITEMS,
|
ITEMS: SETTINGS_ITEMS,
|
||||||
})}
|
})}
|
||||||
|
</li>
|
||||||
|
<li className="nav-item order-2">
|
||||||
{this.getDropdown({
|
{this.getDropdown({
|
||||||
order: 2,
|
order: 2,
|
||||||
label: 'filters',
|
label: 'filters',
|
||||||
|
@ -125,9 +171,10 @@ class Menu extends Component {
|
||||||
URLS: FILTERS_URLS,
|
URLS: FILTERS_URLS,
|
||||||
ITEMS: FILTERS_ITEMS,
|
ITEMS: FILTERS_ITEMS,
|
||||||
})}
|
})}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,13 @@ const getDomainCell = (props) => {
|
||||||
|
|
||||||
const hasTracker = !!tracker;
|
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--active': answer_dnssec,
|
||||||
'icon--disabled': !answer_dnssec,
|
'icon--disabled': !answer_dnssec,
|
||||||
'my-3': isDetailed,
|
'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--active': hasTracker,
|
||||||
'icon--disabled': !hasTracker,
|
'icon--disabled': !hasTracker,
|
||||||
'my-3': isDetailed,
|
'my-3': isDetailed,
|
||||||
|
@ -56,7 +56,7 @@ const getDomainCell = (props) => {
|
||||||
|
|
||||||
const renderGrid = (content, idx) => {
|
const renderGrid = (content, idx) => {
|
||||||
const preparedContent = typeof content === 'string' ? t(content) : content;
|
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,
|
'overflow-break': preparedContent.length > 100,
|
||||||
});
|
});
|
||||||
return <div key={idx} className={className}>{preparedContent}</div>;
|
return <div key={idx} className={className}>{preparedContent}</div>;
|
||||||
|
|
|
@ -2,17 +2,20 @@ import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { Field, reduxForm } from 'redux-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import debounce from 'lodash/debounce';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useHistory } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
DEBOUNCE_FILTER_TIMEOUT,
|
DEBOUNCE_FILTER_TIMEOUT,
|
||||||
DEFAULT_LOGS_FILTER,
|
DEFAULT_LOGS_FILTER,
|
||||||
FORM_NAME,
|
FORM_NAME,
|
||||||
RESPONSE_FILTER,
|
RESPONSE_FILTER,
|
||||||
|
RESPONSE_FILTER_QUERIES,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import Tooltip from '../../ui/Tooltip';
|
import IconTooltip from '../../ui/IconTooltip';
|
||||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||||
|
import useDebounce from '../../../helpers/useDebounce';
|
||||||
|
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||||
|
|
||||||
const renderFilterField = ({
|
const renderFilterField = ({
|
||||||
input,
|
input,
|
||||||
|
@ -25,7 +28,12 @@ const renderFilterField = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
meta: { touched, error },
|
meta: { touched, error },
|
||||||
onClearInputClick,
|
onClearInputClick,
|
||||||
}) => <>
|
onKeyDown,
|
||||||
|
normalizeOnBlur,
|
||||||
|
}) => {
|
||||||
|
const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur);
|
||||||
|
|
||||||
|
return <>
|
||||||
<div className="input-group-search input-group-search__icon--magnifier">
|
<div className="input-group-search input-group-search__icon--magnifier">
|
||||||
<svg className="icons icon--small icon--gray">
|
<svg className="icons icon--small icon--gray">
|
||||||
<use xlinkHref="#magnifier" />
|
<use xlinkHref="#magnifier" />
|
||||||
|
@ -39,7 +47,10 @@ const renderFilterField = ({
|
||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
aria-label={placeholder} />
|
aria-label={placeholder}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
|
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
|
||||||
<svg className="icons icon--smallest icon--gray" onClick={onClearInputClick}>
|
<svg className="icons icon--smallest icon--gray" onClick={onClearInputClick}>
|
||||||
|
@ -47,12 +58,13 @@ const renderFilterField = ({
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="input-group-search input-group-search__icon--tooltip">
|
<span className="input-group-search input-group-search__icon--tooltip">
|
||||||
<Tooltip text={tooltip} type='tooltip-custom--logs' />
|
<IconTooltip text={tooltip} type='tooltip-custom--logs' />
|
||||||
</span>
|
</span>
|
||||||
{!disabled
|
{!disabled
|
||||||
&& touched
|
&& touched
|
||||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
</>;
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
renderFilterField.propTypes = {
|
renderFilterField.propTypes = {
|
||||||
input: PropTypes.object.isRequired,
|
input: PropTypes.object.isRequired,
|
||||||
|
@ -64,65 +76,91 @@ renderFilterField.propTypes = {
|
||||||
disabled: PropTypes.string,
|
disabled: PropTypes.string,
|
||||||
autoComplete: PropTypes.string,
|
autoComplete: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
normalizeOnBlur: PropTypes.func,
|
||||||
meta: PropTypes.shape({
|
meta: PropTypes.shape({
|
||||||
touched: PropTypes.bool,
|
touched: PropTypes.bool,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FORM_NAMES = {
|
||||||
|
search: 'search',
|
||||||
|
response_status: 'response_status',
|
||||||
|
};
|
||||||
|
|
||||||
const Form = (props) => {
|
const Form = (props) => {
|
||||||
const {
|
const {
|
||||||
className = '',
|
className = '',
|
||||||
responseStatusClass,
|
responseStatusClass,
|
||||||
submit,
|
|
||||||
reset,
|
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
|
change,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT);
|
const {
|
||||||
const zeroDelaySubmit = () => setTimeout(submit, 0);
|
response_status, search,
|
||||||
|
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||||
|
|
||||||
const clearInput = async () => {
|
const [
|
||||||
await dispatch(setLogsFilter(DEFAULT_LOGS_FILTER));
|
debouncedSearch,
|
||||||
await reset();
|
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 () => {
|
const onInputClear = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await clearInput();
|
setDebouncedSearch(DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||||
|
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => clearInput, []);
|
const onEnterPress = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setDebouncedSearch(search);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeOnBlur = (data) => data.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="d-flex flex-wrap form-control--container"
|
<form className="d-flex flex-wrap form-control--container"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
zeroDelaySubmit();
|
|
||||||
debouncedSubmit.cancel();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
id="search"
|
id={FORM_NAMES.search}
|
||||||
name="search"
|
name={FORM_NAMES.search}
|
||||||
component={renderFilterField}
|
component={renderFilterField}
|
||||||
type="text"
|
type="text"
|
||||||
className={classNames('form-control--search form-control--transparent', className)}
|
className={classNames('form-control--search form-control--transparent', className)}
|
||||||
placeholder={t('domain_or_client')}
|
placeholder={t('domain_or_client')}
|
||||||
tooltip={t('query_log_strict_search')}
|
tooltip={t('query_log_strict_search')}
|
||||||
onChange={debouncedSubmit}
|
|
||||||
onClearInputClick={onInputClear}
|
onClearInputClick={onInputClear}
|
||||||
|
onKeyDown={onEnterPress}
|
||||||
|
normalizeOnBlur={normalizeOnBlur}
|
||||||
/>
|
/>
|
||||||
<div className="field__select">
|
<div className="field__select">
|
||||||
<Field
|
<Field
|
||||||
name="response_status"
|
name={FORM_NAMES.response_status}
|
||||||
component="select"
|
component="select"
|
||||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
|
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)
|
{Object.values(RESPONSE_FILTER)
|
||||||
.map(({
|
.map(({
|
||||||
|
@ -136,14 +174,13 @@ const Form = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
Form.propTypes = {
|
Form.propTypes = {
|
||||||
handleChange: PropTypes.func,
|
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
responseStatusClass: PropTypes.string,
|
responseStatusClass: PropTypes.string,
|
||||||
submit: PropTypes.func.isRequired,
|
change: PropTypes.func.isRequired,
|
||||||
reset: PropTypes.func.isRequired,
|
|
||||||
setIsLoading: PropTypes.func.isRequired,
|
setIsLoading: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default reduxForm({
|
||||||
form: FORM_NAME.LOGS_FILTER,
|
form: FORM_NAME.LOGS_FILTER,
|
||||||
|
enableReinitialize: true,
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|
|
@ -1,20 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
|
||||||
|
|
||||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
const Filters = ({ filter, refreshLogs, setIsLoading }) => (
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const onSubmit = async (values) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
await dispatch(setLogsFilter(values));
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="page-header page-header--logs">
|
<div className="page-header page-header--logs">
|
||||||
<h1 className="page-title page-title--large">
|
<h1 className="page-title page-title--large">
|
||||||
<Trans>query_log</Trans>
|
<Trans>query_log</Trans>
|
||||||
|
@ -27,17 +16,14 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||||
<use xlinkHref="#update" />
|
<use xlinkHref="#update" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</h1>
|
</h1>
|
||||||
<Form
|
<Form
|
||||||
responseStatusClass="d-sm-block"
|
responseStatusClass="d-sm-block"
|
||||||
initialValues={filter}
|
initialValues={filter}
|
||||||
onSubmit={onSubmit}
|
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
Filters.propTypes = {
|
Filters.propTypes = {
|
||||||
filter: PropTypes.object.isRequired,
|
filter: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -49,7 +49,7 @@ const Table = (props) => {
|
||||||
isLoading,
|
isLoading,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toggleBlocking = (type, domain) => {
|
const toggleBlocking = (type, domain) => {
|
||||||
const {
|
const {
|
||||||
|
@ -239,7 +239,7 @@ const Table = (props) => {
|
||||||
sortable={false}
|
sortable={false}
|
||||||
resizable={false}
|
resizable={false}
|
||||||
data={logs || []}
|
data={logs || []}
|
||||||
loading={isLoading}
|
loading={isLoading || processingGetLogs}
|
||||||
showPageJump={false}
|
showPageJump={false}
|
||||||
showPageSizeOptions={false}
|
showPageSizeOptions={false}
|
||||||
onPageChange={changePage}
|
onPageChange={changePage}
|
||||||
|
|
|
@ -2,11 +2,14 @@ import React, { Fragment, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import Modal from 'react-modal';
|
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 {
|
import {
|
||||||
BLOCK_ACTIONS, smallScreenSize,
|
BLOCK_ACTIONS,
|
||||||
TABLE_DEFAULT_PAGE_SIZE,
|
TABLE_DEFAULT_PAGE_SIZE,
|
||||||
TABLE_FIRST_PAGE,
|
TABLE_FIRST_PAGE,
|
||||||
|
smallScreenSize,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import Filters from './Filters';
|
import Filters from './Filters';
|
||||||
|
@ -15,13 +18,15 @@ import Disabled from './Disabled';
|
||||||
import { getFilteringStatus } from '../../actions/filtering';
|
import { getFilteringStatus } from '../../actions/filtering';
|
||||||
import { getClients } from '../../actions';
|
import { getClients } from '../../actions';
|
||||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||||
import { getLogsConfig } from '../../actions/queryLogs';
|
import {
|
||||||
|
getLogsConfig,
|
||||||
|
refreshFilteredLogs,
|
||||||
|
resetFilteredLogs,
|
||||||
|
setFilteredLogs,
|
||||||
|
} from '../../actions/queryLogs';
|
||||||
import { addSuccessToast } from '../../actions/toasts';
|
import { addSuccessToast } from '../../actions/toasts';
|
||||||
import './Logs.css';
|
import './Logs.css';
|
||||||
|
|
||||||
const INITIAL_REQUEST = true;
|
|
||||||
const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST];
|
|
||||||
|
|
||||||
export const processContent = (data, buttonType) => Object.entries(data)
|
export const processContent = (data, buttonType) => Object.entries(data)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -56,22 +61,44 @@ export const processContent = (data, buttonType) => Object.entries(data)
|
||||||
|
|
||||||
const Logs = (props) => {
|
const Logs = (props) => {
|
||||||
const dispatch = useDispatch();
|
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 [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize);
|
||||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||||
const [isModalOpened, setModalOpened] = useState(false);
|
const [isModalOpened, setModalOpened] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await dispatch(setFilteredLogs({
|
||||||
|
search,
|
||||||
|
response_status,
|
||||||
|
}));
|
||||||
|
setIsLoading(false);
|
||||||
|
})();
|
||||||
|
}, [response_status, search]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filtering,
|
filtering,
|
||||||
setLogsPage,
|
setLogsPage,
|
||||||
setLogsPagination,
|
setLogsPagination,
|
||||||
setLogsFilter,
|
|
||||||
toggleDetailedLogs,
|
toggleDetailedLogs,
|
||||||
dashboard,
|
dashboard,
|
||||||
dnsConfig,
|
dnsConfig,
|
||||||
queryLogs: {
|
queryLogs: {
|
||||||
filter,
|
|
||||||
enabled,
|
enabled,
|
||||||
processingGetConfig,
|
processingGetConfig,
|
||||||
processingAdditionalLogs,
|
processingAdditionalLogs,
|
||||||
|
@ -92,16 +119,10 @@ const Logs = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mediaQuery.addListener(mediaQueryHandler);
|
|
||||||
|
|
||||||
return () => mediaQuery.removeListener(mediaQueryHandler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeModal = () => setModalOpened(false);
|
const closeModal = () => setModalOpened(false);
|
||||||
|
|
||||||
const getLogs = (older_than, page, initial) => {
|
const getLogs = (older_than, page, initial) => {
|
||||||
if (props.queryLogs.enabled) {
|
if (enabled) {
|
||||||
props.getLogs({
|
props.getLogs({
|
||||||
older_than,
|
older_than,
|
||||||
page,
|
page,
|
||||||
|
@ -112,6 +133,8 @@ const Logs = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
mediaQuery.addEventListener('change', mediaQueryHandler);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||||
|
@ -119,7 +142,6 @@ const Logs = (props) => {
|
||||||
dispatch(getClients());
|
dispatch(getClients());
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getLogs(...INITIAL_REQUEST_DATA),
|
|
||||||
dispatch(getLogsConfig()),
|
dispatch(getLogsConfig()),
|
||||||
dispatch(getDnsConfig()),
|
dispatch(getDnsConfig()),
|
||||||
]);
|
]);
|
||||||
|
@ -129,13 +151,18 @@ const Logs = (props) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
||||||
|
dispatch(resetFilteredLogs());
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshLogs = async () => {
|
const refreshLogs = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||||
getLogs(...INITIAL_REQUEST_DATA),
|
dispatch(refreshFilteredLogs()),
|
||||||
]);
|
]);
|
||||||
dispatch(addSuccessToast('query_log_updated'));
|
dispatch(addSuccessToast('query_log_updated'));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -145,13 +172,15 @@ const Logs = (props) => {
|
||||||
<>
|
<>
|
||||||
{enabled && processingGetConfig && <Loading />}
|
{enabled && processingGetConfig && <Loading />}
|
||||||
{enabled && !processingGetConfig && (
|
{enabled && !processingGetConfig && (
|
||||||
<Fragment>
|
<>
|
||||||
<Filters
|
<Filters
|
||||||
filter={filter}
|
filter={{
|
||||||
|
response_status,
|
||||||
|
search,
|
||||||
|
}}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
processingGetLogs={processingGetLogs}
|
processingGetLogs={processingGetLogs}
|
||||||
processingAdditionalLogs={processingAdditionalLogs}
|
processingAdditionalLogs={processingAdditionalLogs}
|
||||||
setLogsFilter={setLogsFilter}
|
|
||||||
refreshLogs={refreshLogs}
|
refreshLogs={refreshLogs}
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
|
@ -201,7 +230,7 @@ const Logs = (props) => {
|
||||||
</svg>
|
</svg>
|
||||||
{processContent(detailedDataCurrent, buttonType)}
|
{processContent(detailedDataCurrent, buttonType)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
{!enabled && !processingGetConfig && (
|
{!enabled && !processingGetConfig && (
|
||||||
<Disabled />
|
<Disabled />
|
||||||
|
@ -219,7 +248,6 @@ Logs.propTypes = {
|
||||||
setRules: PropTypes.func.isRequired,
|
setRules: PropTypes.func.isRequired,
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
addSuccessToast: PropTypes.func.isRequired,
|
||||||
setLogsPagination: PropTypes.func.isRequired,
|
setLogsPagination: PropTypes.func.isRequired,
|
||||||
setLogsFilter: PropTypes.func.isRequired,
|
|
||||||
setLogsPage: PropTypes.func.isRequired,
|
setLogsPage: PropTypes.func.isRequired,
|
||||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||||
dnsConfig: PropTypes.object.isRequired,
|
dnsConfig: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Card from '../../ui/Card';
|
||||||
import CellWrap from '../../ui/CellWrap';
|
import CellWrap from '../../ui/CellWrap';
|
||||||
|
|
||||||
import whoisCell from './whoisCell';
|
import whoisCell from './whoisCell';
|
||||||
|
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||||
|
|
||||||
const COLUMN_MIN_WIDTH = 200;
|
const COLUMN_MIN_WIDTH = 200;
|
||||||
|
|
||||||
|
@ -49,7 +50,9 @@ class AutoClients extends Component {
|
||||||
return (
|
return (
|
||||||
<div className="logs__row">
|
<div className="logs__row">
|
||||||
<div className="logs__text" title={clientStats}>
|
<div className="logs__text" title={clientStats}>
|
||||||
|
<LogsSearchLink search={row.original.ip}>
|
||||||
{clientStats}
|
{clientStats}
|
||||||
|
</LogsSearchLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { normalizeTextarea } from '../../../helpers/helpers';
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import CellWrap from '../../ui/CellWrap';
|
import CellWrap from '../../ui/CellWrap';
|
||||||
|
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||||
|
|
||||||
class ClientsTable extends Component {
|
class ClientsTable extends Component {
|
||||||
handleFormAdd = (values) => {
|
handleFormAdd = (values) => {
|
||||||
|
@ -49,7 +50,10 @@ class ClientsTable extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
getOptionsWithLabels = (options) => (
|
getOptionsWithLabels = (options) => (
|
||||||
options.map((option) => ({ value: option, label: option }))
|
options.map((option) => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
getClient = (name, clients) => {
|
getClient = (name, clients) => {
|
||||||
|
@ -203,7 +207,15 @@ class ClientsTable extends Component {
|
||||||
accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0,
|
accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0,
|
||||||
sortMethod: (a, b) => b - a,
|
sortMethod: (a, b) => b - a,
|
||||||
minWidth: 120,
|
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'),
|
Header: this.props.t('actions_table_header'),
|
||||||
|
@ -311,7 +323,6 @@ class ClientsTable extends Component {
|
||||||
>
|
>
|
||||||
<Trans>client_add</Trans>
|
<Trans>client_add</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
modalType={modalType}
|
modalType={modalType}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import LogsSearchLink from './LogsSearchLink';
|
||||||
import { formatNumber } from '../../helpers/helpers';
|
import { formatNumber } from '../../helpers/helpers';
|
||||||
|
|
||||||
const Cell = ({ value, percent, color }) => (
|
const Cell = ({
|
||||||
<div className="stats__row">
|
value, percent, color, search,
|
||||||
|
}) => <div className="stats__row">
|
||||||
<div className="stats__row-value mb-1">
|
<div className="stats__row-value mb-1">
|
||||||
<strong>{formatNumber(value)}</strong>
|
<strong><LogsSearchLink search={search}>{formatNumber(value)}</LogsSearchLink></strong>
|
||||||
<small className="ml-3 text-muted">{percent}%</small>
|
<small className="ml-3 text-muted">{percent}%</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="progress progress-xs">
|
<div className="progress progress-xs">
|
||||||
|
@ -18,13 +19,14 @@ const Cell = ({ value, percent, color }) => (
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
);
|
|
||||||
|
|
||||||
Cell.propTypes = {
|
Cell.propTypes = {
|
||||||
value: PropTypes.number.isRequired,
|
value: PropTypes.number.isRequired,
|
||||||
percent: PropTypes.number.isRequired,
|
percent: PropTypes.number.isRequired,
|
||||||
color: PropTypes.string.isRequired,
|
color: PropTypes.string.isRequired,
|
||||||
|
search: PropTypes.string,
|
||||||
|
onSearchRedirect: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
.stats__link {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats__link:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -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 { connect } from 'react-redux';
|
||||||
import { getFilteringStatus, setRules } from '../actions/filtering';
|
import { getFilteringStatus, setRules } from '../actions/filtering';
|
||||||
import {
|
import {
|
||||||
getLogs, setLogsPagination, setLogsFilter, setLogsPage, toggleDetailedLogs,
|
getLogs, setLogsPagination, setLogsPage, toggleDetailedLogs,
|
||||||
} from '../actions/queryLogs';
|
} from '../actions/queryLogs';
|
||||||
import Logs from '../components/Logs';
|
import Logs from '../components/Logs';
|
||||||
import { addSuccessToast } from '../actions/toasts';
|
import { addSuccessToast } from '../actions/toasts';
|
||||||
|
@ -26,7 +26,6 @@ const mapDispatchToProps = {
|
||||||
setRules,
|
setRules,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
setLogsPagination,
|
setLogsPagination,
|
||||||
setLogsFilter,
|
|
||||||
setLogsPage,
|
setLogsPage,
|
||||||
toggleDetailedLogs,
|
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 = {
|
export const FILTERED_STATUS_TO_META_MAP = {
|
||||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||||
label: RESPONSE_FILTER.ALLOWED.label,
|
label: RESPONSE_FILTER.ALLOWED.label,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import axios from 'axios';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import ipaddr from 'ipaddr.js';
|
import ipaddr from 'ipaddr.js';
|
||||||
|
import queryString from 'query-string';
|
||||||
import versionCompare from './versionCompare';
|
import versionCompare from './versionCompare';
|
||||||
import { getTrackerData } from './trackers/trackers';
|
import { getTrackerData } from './trackers/trackers';
|
||||||
|
|
||||||
|
@ -618,6 +619,16 @@ export const selectCompletedFields = (values) => Object.entries(values)
|
||||||
return acc;
|
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)
|
export const processContent = (content) => (Array.isArray(content)
|
||||||
? content.filter(([, value]) => value)
|
? content.filter(([, value]) => value)
|
||||||
|
|
|
@ -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,
|
page: payload,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.setLogsFilterRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
[actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
||||||
[actions.setLogsFilterFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
[actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||||
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
isDetailed: payload,
|
isDetailed: payload,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.setLogsFilterSuccess]: (state, { payload }) => {
|
[actions.setFilteredLogsSuccess]: (state, { payload }) => {
|
||||||
const { logs, oldest, filter } = payload;
|
const { logs, oldest, filter } = payload;
|
||||||
const pageSize = TABLE_DEFAULT_PAGE_SIZE;
|
const pageSize = TABLE_DEFAULT_PAGE_SIZE;
|
||||||
const page = 0;
|
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.getLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
||||||
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||||
[actions.getLogsSuccess]: (state, { payload }) => {
|
[actions.getLogsSuccess]: (state, { payload }) => {
|
||||||
|
|
Loading…
Reference in New Issue