client: 2451 Support multiple matched rules in the UI

Close #2451

Squashed commit of the following:

commit f6d0a8fe4fa575dff67b2d2f4c9f6aaf6a6414d3
Merge: 3b13e529d 955b735c8
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Fri Dec 25 14:53:16 2020 +0300

    Merge branch 'master' into feature/2451

commit 3b13e529da01823cbc674d81be065b68cd08bbd3
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Thu Dec 24 12:53:13 2020 +0300

    Update JSXElement in jsdocs

commit f0749cd0466ef69d964b1c2575dffacb33f71b31
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Dec 21 13:23:48 2020 +0300

    minor

commit bd014b00e762a1895c132bc962c06f107c50fe17
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Dec 21 12:49:29 2020 +0300

    Minor helper update

commit 260a66b7b78eb80596b88fec14f409838727e4bb
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Dec 21 12:31:08 2020 +0300

    Rule locale update

commit c960cf9f658e52cb587676dceb97506880a9db94
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Dec 21 12:27:50 2020 +0300

    Add styles for filters list

commit 6f3b2176fd52598cddb147ad7828adb95abf08f0
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Fri Dec 18 18:34:17 2020 +0300

    client: 2451 Support multiple matched rules in the UI
This commit is contained in:
Artem Baskal 2020-12-25 15:03:37 +03:00
parent 955b735c8b
commit 644a9b5565
8 changed files with 134 additions and 40 deletions

View File

@ -1846,7 +1846,7 @@ Response:
} }
There are also deprecated properties `filter_id` and `rule` on the top level of There are also deprecated properties `filter_id` and `rule` on the top level of
the response object. Their usaga should be replaced with the response object. Their usage should be replaced with
`rules[*].filter_list_id` and `rules[*].text` correspondingly. See the `rules[*].filter_list_id` and `rules[*].text` correspondingly. See the
_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. _OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file.

View File

@ -270,7 +270,7 @@
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",
"rule_label": "Rule", "rule_label": "Rule(s)",
"list_label": "List", "list_label": "List",
"unknown_filter": "Unknown filter {{filterId}}", "unknown_filter": "Unknown filter {{filterId}}",
"known_tracker": "Known tracker", "known_tracker": "Known tracker",
@ -530,7 +530,6 @@
"check_ip": "IP addresses: {{ip}}", "check_ip": "IP addresses: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Reason: {{reason}}", "check_reason": "Reason: {{reason}}",
"check_rule": "Rule: {{rule}}",
"check_service": "Service name: {{service}}", "check_service": "Service name: {{service}}",
"service_name": "Service name", "service_name": "Service name",
"check_not_found": "Not found in your filter lists", "check_not_found": "Not found in your filter lists",

View File

@ -12,7 +12,7 @@ import {
checkSafeSearch, checkSafeSearch,
checkSafeBrowsing, checkSafeBrowsing,
checkParental, checkParental,
getFilterName, getRulesToFilterList,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants'; import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';
import { toggleBlocking } from '../../../actions'; import { toggleBlocking } from '../../../actions';
@ -41,32 +41,27 @@ const renderBlockingButton = (isFiltered, domain) => {
</button>; </button>;
}; };
const getTitle = (reason) => { const getTitle = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const filters = useSelector((state) => state.filtering.filters, shallowEqual); const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const filter_id = useSelector((state) => state.filtering.check.filter_id); const rules = useSelector((state) => state.filtering.check.rules, shallowEqual);
const reason = useSelector((state) => state.filtering.check.reason);
const filterName = getFilterName(
filters,
whitelistFilters,
filter_id,
'filtered_custom_rules',
(filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''),
);
const getReasonFiltered = (reason) => { const getReasonFiltered = (reason) => {
const filterKey = reason.replace(FILTERED, ''); const filterKey = reason.replace(FILTERED, '');
return i18next.t('query_log_filtered', { filter: filterKey }); return i18next.t('query_log_filtered', { filter: filterKey });
}; };
const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters);
const REASON_TO_TITLE_MAP = { const REASON_TO_TITLE_MAP = {
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'), [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'),
[FILTERED_STATUS.REWRITE]: t('rewrite_applied'), [FILTERED_STATUS.REWRITE]: t('rewrite_applied'),
[FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'), [FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'),
[FILTERED_STATUS.FILTERED_BLACK_LIST]: filterName, [FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames,
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: filterName, [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames,
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason),
@ -78,7 +73,11 @@ const getTitle = (reason) => {
return <> return <>
<div>{t('check_reason', { reason })}</div> <div>{t('check_reason', { reason })}</div>
<div>{filterName}</div> <div>
{t('rule_label')}:
&nbsp;
{ruleAndFilterNames}
</div>
</>; </>;
}; };
@ -86,14 +85,13 @@ const Info = () => {
const { const {
hostname, hostname,
reason, reason,
rule,
service_name, service_name,
cname, cname,
ip_addrs, ip_addrs,
} = useSelector((state) => state.filtering.check, shallowEqual); } = useSelector((state) => state.filtering.check, shallowEqual);
const { t } = useTranslation(); const { t } = useTranslation();
const title = getTitle(reason); const title = getTitle();
const className = classNames('card mb-0 p-3', { const className = classNames('card mb-0 p-3', {
'logs__row--red': checkFiltered(reason), 'logs__row--red': checkFiltered(reason),
@ -112,7 +110,6 @@ const Info = () => {
<div>{title}</div> <div>{title}</div>
{!onlyFiltered {!onlyFiltered
&& <> && <>
{rule && <div>{t('check_rule', { rule })}</div>}
{service_name && <div>{t('check_service', { service: service_name })}</div>} {service_name && <div>{t('check_service', { service: service_name })}</div>}
{cname && <div>{t('check_cname', { cname })}</div>} {cname && <div>{t('check_cname', { cname })}</div>}
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>} {ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}

View File

@ -4,8 +4,9 @@ import classNames from 'classnames';
import React from 'react'; import React from 'react';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { import {
getRulesToFilterList,
formatElapsedMs, formatElapsedMs,
getFilterName, getFilterNames,
getServiceName, getServiceName,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants'; import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
@ -18,8 +19,7 @@ const ResponseCell = ({
response, response,
status, status,
upstream, upstream,
rule, rules,
filterId,
service_name, service_name,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -36,7 +36,6 @@ const ResponseCell = ({
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason); const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>; const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
const filter = getFilterName(filters, whitelistFilters, filterId);
const renderResponses = (responseArr) => { const renderResponses = (responseArr) => {
if (!responseArr || responseArr.length === 0) { if (!responseArr || responseArr.length === 0) {
@ -52,18 +51,23 @@ const ResponseCell = ({
})}</div>; })}</div>;
}; };
const rulesList = getRulesToFilterList(rules, filters, whitelistFilters);
const COMMON_CONTENT = { const COMMON_CONTENT = {
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
response_code: status, response_code: status,
...(service_name ? { service_name: getServiceName(service_name) } : { filter }), ...(service_name
rule_label: rule, ? { service_name: getServiceName(service_name) }
: { }
),
rule_label: rulesList,
response_table_header: renderResponses(response), response_table_header: renderResponses(response),
original_response: renderResponses(originalResponse), original_response: renderResponses(originalResponse),
}; };
const content = rule const content = rules.length > 0
? Object.entries(COMMON_CONTENT) ? Object.entries(COMMON_CONTENT)
: Object.entries({ : Object.entries({
...COMMON_CONTENT, ...COMMON_CONTENT,
@ -78,7 +82,8 @@ const ResponseCell = ({
} }
return getServiceName(service_name); return getServiceName(service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST: case FILTERED_STATUS.FILTERED_BLACK_LIST:
return filter; case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
default: default:
return formattedElapsedMs; return formattedElapsedMs;
} }
@ -113,8 +118,10 @@ ResponseCell.propTypes = {
response: propTypes.array.isRequired, response: propTypes.array.isRequired,
status: propTypes.string.isRequired, status: propTypes.string.isRequired,
upstream: propTypes.string.isRequired, upstream: propTypes.string.isRequired,
rule: propTypes.string, rules: propTypes.arrayOf(propTypes.shape({
filterId: propTypes.number, text: propTypes.string.isRequired,
filter_list_id: propTypes.number.isRequired,
})),
service_name: propTypes.string, service_name: propTypes.string,
}; };

View File

@ -6,11 +6,11 @@ import propTypes from 'prop-types';
import { import {
captitalizeWords, captitalizeWords,
checkFiltered, checkFiltered,
getRulesToFilterList,
formatDateTime, formatDateTime,
formatElapsedMs, formatElapsedMs,
formatTime, formatTime,
getBlockingClientName, getBlockingClientName,
getFilterName,
getServiceName, getServiceName,
processContent, processContent,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
@ -70,8 +70,7 @@ const Row = memo(({
upstream, upstream,
type, type,
client_proto, client_proto,
filterId, rules,
rule,
originalResponse, originalResponse,
status, status,
service_name, service_name,
@ -107,8 +106,6 @@ const Row = memo(({
const sourceData = getSourceData(tracker); const sourceData = getSourceData(tracker);
const filter = getFilterName(filters, whitelistFilters, filterId);
const { const {
confirmMessage, confirmMessage,
buttonKey: blockingClientKey, buttonKey: blockingClientKey,
@ -172,8 +169,8 @@ const Row = memo(({
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
filter: rule ? filter : null, rule_label: rules.length > 0
rule_label: rule, && getRulesToFilterList(rules, filters, whitelistFilters),
response_table_header: response?.join('\n'), response_table_header: response?.join('\n'),
response_code: status, response_code: status,
client_details: 'title', client_details: 'title',
@ -235,8 +232,10 @@ Row.propTypes = {
upstream: propTypes.string.isRequired, upstream: propTypes.string.isRequired,
type: propTypes.string.isRequired, type: propTypes.string.isRequired,
client_proto: propTypes.string.isRequired, client_proto: propTypes.string.isRequired,
filterId: propTypes.number, rules: propTypes.arrayOf(propTypes.shape({
rule: propTypes.string, text: propTypes.string.isRequired,
filter_list_id: propTypes.number.isRequired,
})),
originalResponse: propTypes.array, originalResponse: propTypes.array,
status: propTypes.string.isRequired, status: propTypes.string.isRequired,
service_name: propTypes.string, service_name: propTypes.string,

View File

@ -428,3 +428,13 @@
margin-right: 1px; margin-right: 1px;
opacity: 0.5; opacity: 0.5;
} }
.filteringRules__rule {
margin-bottom: 0;
}
.filteringRules__filter {
font-style: italic;
font-weight: normal;
margin-bottom: 1rem;
}

View File

@ -7,6 +7,7 @@ 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 queryString from 'query-string';
import React from 'react';
import { getTrackerData } from './trackers/trackers'; import { getTrackerData } from './trackers/trackers';
import { import {
@ -68,6 +69,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
time, time,
filterId, filterId,
rule, rule,
rules,
service_name, service_name,
original_answer, original_answer,
upstream, upstream,
@ -80,6 +82,15 @@ export const normalizeLogs = (logs) => logs.map((log) => {
return `${type}: ${value} (ttl=${ttl})`; return `${type}: ${value} (ttl=${ttl})`;
}) : []); }) : []);
let newRules = rules;
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
if (rule !== undefined && filterId !== undefined && rules !== undefined && rules.length === 0) {
newRules = {
filter_list_id: filterId,
text: rule,
};
}
return { return {
time, time,
domain, domain,
@ -88,8 +99,10 @@ export const normalizeLogs = (logs) => logs.map((log) => {
reason, reason,
client, client,
client_proto, client_proto,
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
filterId, filterId,
rule, rule,
rules: newRules,
status, status,
service_name, service_name,
originalAnswer: original_answer, originalAnswer: original_answer,
@ -726,6 +739,75 @@ export const getFilterName = (
return resolveFilterName(filter); return resolveFilterName(filter);
}; };
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {string[]}
*/
export const getFilterNames = (rules, filters, whitelistFilters) => rules.map(
({ filter_list_id }) => getFilterName(filters, whitelistFilters, filter_list_id),
);
/**
* @param {array} rules
* @returns {string[]}
*/
export const getRuleNames = (rules) => rules.map(({ text }) => text);
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {object}
*/
export const getFilterNameToRulesMap = (rules, filters, whitelistFilters) => rules.reduce(
(acc, { text, filter_list_id }) => {
const filterName = getFilterName(filters, whitelistFilters, filter_list_id);
acc[filterName] = (acc[filterName] || []).concat(text);
return acc;
}, {},
);
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @param {object} classes
* @returns {JSXElement}
*/
export const getRulesToFilterList = (rules, filters, whitelistFilters, classes = {
list: 'filteringRules',
rule: 'filteringRules__rule font-monospace',
filter: 'filteringRules__filter',
}) => {
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
return <dl className={classes.list}>
{Object.entries(filterNameToRulesMap).reduce(
(acc, [filterName, rulesArr]) => acc
.concat(rulesArr.map((rule, i) => <dd key={i} className={classes.rule}>{rule}</dd>))
.concat(<dt className={classes.filter} key={classes.filter}>{filterName}</dt>),
[],
)}
</dl>;
};
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {string}
*/
export const getRulesAndFilterNames = (rules, filters, whitelistFilters) => {
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
return Object.entries(filterNameToRulesMap).map(
([filterName, filterRules]) => filterRules.concat(filterName).join('\n'),
).join('\n\n');
};
/** /**
* @param ip {string} * @param ip {string}
* @param gateway_ip {string} * @param gateway_ip {string}

View File

@ -31,7 +31,7 @@ const getFormattedWhois = (whois) => {
* @param {object} info.whois_info * @param {object} info.whois_info
* @param {boolean} [isDetailed] * @param {boolean} [isDetailed]
* @param {boolean} [isLogs] * @param {boolean} [isLogs]
* @returns {JSX.Element} * @returns {JSXElement}
*/ */
export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => { export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => {
let whoisContainer = null; let whoisContainer = null;