From 644a9b55651af382f524aba6ab5e954bb4f4bb35 Mon Sep 17 00:00:00 2001 From: Artem Baskal Date: Fri, 25 Dec 2020 15:03:37 +0300 Subject: [PATCH] client: 2451 Support multiple matched rules in the UI Close #2451 Squashed commit of the following: commit f6d0a8fe4fa575dff67b2d2f4c9f6aaf6a6414d3 Merge: 3b13e529d 955b735c8 Author: Artem Baskal Date: Fri Dec 25 14:53:16 2020 +0300 Merge branch 'master' into feature/2451 commit 3b13e529da01823cbc674d81be065b68cd08bbd3 Author: Artem Baskal Date: Thu Dec 24 12:53:13 2020 +0300 Update JSXElement in jsdocs commit f0749cd0466ef69d964b1c2575dffacb33f71b31 Author: Artem Baskal Date: Mon Dec 21 13:23:48 2020 +0300 minor commit bd014b00e762a1895c132bc962c06f107c50fe17 Author: Artem Baskal Date: Mon Dec 21 12:49:29 2020 +0300 Minor helper update commit 260a66b7b78eb80596b88fec14f409838727e4bb Author: Artem Baskal Date: Mon Dec 21 12:31:08 2020 +0300 Rule locale update commit c960cf9f658e52cb587676dceb97506880a9db94 Author: Artem Baskal Date: Mon Dec 21 12:27:50 2020 +0300 Add styles for filters list commit 6f3b2176fd52598cddb147ad7828adb95abf08f0 Author: Artem Baskal Date: Fri Dec 18 18:34:17 2020 +0300 client: 2451 Support multiple matched rules in the UI --- AGHTechDoc.md | 2 +- client/src/__locales/en.json | 3 +- client/src/components/Filters/Check/Info.js | 31 ++++--- .../src/components/Logs/Cells/ResponseCell.js | 27 +++--- client/src/components/Logs/Cells/index.js | 17 ++-- client/src/components/Logs/Logs.css | 10 +++ client/src/helpers/helpers.js | 82 +++++++++++++++++++ .../src/helpers/renderFormattedClientCell.js | 2 +- 8 files changed, 134 insertions(+), 40 deletions(-) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index b91b8586..ed7f48ba 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -1846,7 +1846,7 @@ Response: } 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 _OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 388691b0..afc2f105 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -270,7 +270,7 @@ "source_label": "Source", "found_in_known_domain_db": "Found in the known domains database.", "category_label": "Category", - "rule_label": "Rule", + "rule_label": "Rule(s)", "list_label": "List", "unknown_filter": "Unknown filter {{filterId}}", "known_tracker": "Known tracker", @@ -530,7 +530,6 @@ "check_ip": "IP addresses: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Reason: {{reason}}", - "check_rule": "Rule: {{rule}}", "check_service": "Service name: {{service}}", "service_name": "Service name", "check_not_found": "Not found in your filter lists", diff --git a/client/src/components/Filters/Check/Info.js b/client/src/components/Filters/Check/Info.js index 1d4c22c5..f53f254d 100644 --- a/client/src/components/Filters/Check/Info.js +++ b/client/src/components/Filters/Check/Info.js @@ -12,7 +12,7 @@ import { checkSafeSearch, checkSafeBrowsing, checkParental, - getFilterName, + getRulesToFilterList, } from '../../../helpers/helpers'; import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants'; import { toggleBlocking } from '../../../actions'; @@ -41,32 +41,27 @@ const renderBlockingButton = (isFiltered, domain) => { ; }; -const getTitle = (reason) => { +const getTitle = () => { const { t } = useTranslation(); const filters = useSelector((state) => state.filtering.filters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); - const filter_id = useSelector((state) => state.filtering.check.filter_id); - - const filterName = getFilterName( - filters, - whitelistFilters, - filter_id, - 'filtered_custom_rules', - (filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''), - ); + const rules = useSelector((state) => state.filtering.check.rules, shallowEqual); + const reason = useSelector((state) => state.filtering.check.reason); const getReasonFiltered = (reason) => { const filterKey = reason.replace(FILTERED, ''); return i18next.t('query_log_filtered', { filter: filterKey }); }; + const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters); + const REASON_TO_TITLE_MAP = { [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'), [FILTERED_STATUS.REWRITE]: t('rewrite_applied'), [FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'), - [FILTERED_STATUS.FILTERED_BLACK_LIST]: filterName, - [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: filterName, + [FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames, + [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames, [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason), @@ -78,7 +73,11 @@ const getTitle = (reason) => { return <>
{t('check_reason', { reason })}
-
{filterName}
+
+ {t('rule_label')}: +   + {ruleAndFilterNames} +
; }; @@ -86,14 +85,13 @@ const Info = () => { const { hostname, reason, - rule, service_name, cname, ip_addrs, } = useSelector((state) => state.filtering.check, shallowEqual); const { t } = useTranslation(); - const title = getTitle(reason); + const title = getTitle(); const className = classNames('card mb-0 p-3', { 'logs__row--red': checkFiltered(reason), @@ -112,7 +110,6 @@ const Info = () => {
{title}
{!onlyFiltered && <> - {rule &&
{t('check_rule', { rule })}
} {service_name &&
{t('check_service', { service: service_name })}
} {cname &&
{t('check_cname', { cname })}
} {ip_addrs &&
{t('check_ip', { ip: ip_addrs.join(', ') })}
} diff --git a/client/src/components/Logs/Cells/ResponseCell.js b/client/src/components/Logs/Cells/ResponseCell.js index 816f35a3..026dbce1 100644 --- a/client/src/components/Logs/Cells/ResponseCell.js +++ b/client/src/components/Logs/Cells/ResponseCell.js @@ -4,8 +4,9 @@ import classNames from 'classnames'; import React from 'react'; import propTypes from 'prop-types'; import { + getRulesToFilterList, formatElapsedMs, - getFilterName, + getFilterNames, getServiceName, } from '../../../helpers/helpers'; import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants'; @@ -18,8 +19,7 @@ const ResponseCell = ({ response, status, upstream, - rule, - filterId, + rules, service_name, }) => { 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 boldStatusLabel = {statusLabel}; - const filter = getFilterName(filters, whitelistFilters, filterId); const renderResponses = (responseArr) => { if (!responseArr || responseArr.length === 0) { @@ -52,18 +51,23 @@ const ResponseCell = ({ })}; }; + const rulesList = getRulesToFilterList(rules, filters, whitelistFilters); + const COMMON_CONTENT = { encryption_status: boldStatusLabel, install_settings_dns: upstream, elapsed: formattedElapsedMs, response_code: status, - ...(service_name ? { service_name: getServiceName(service_name) } : { filter }), - rule_label: rule, + ...(service_name + ? { service_name: getServiceName(service_name) } + : { } + ), + rule_label: rulesList, response_table_header: renderResponses(response), original_response: renderResponses(originalResponse), }; - const content = rule + const content = rules.length > 0 ? Object.entries(COMMON_CONTENT) : Object.entries({ ...COMMON_CONTENT, @@ -78,7 +82,8 @@ const ResponseCell = ({ } return getServiceName(service_name); case FILTERED_STATUS.FILTERED_BLACK_LIST: - return filter; + case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: + return getFilterNames(rules, filters, whitelistFilters).join(', '); default: return formattedElapsedMs; } @@ -113,8 +118,10 @@ ResponseCell.propTypes = { response: propTypes.array.isRequired, status: propTypes.string.isRequired, upstream: propTypes.string.isRequired, - rule: propTypes.string, - filterId: propTypes.number, + rules: propTypes.arrayOf(propTypes.shape({ + text: propTypes.string.isRequired, + filter_list_id: propTypes.number.isRequired, + })), service_name: propTypes.string, }; diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js index 2e2635d9..8435a617 100644 --- a/client/src/components/Logs/Cells/index.js +++ b/client/src/components/Logs/Cells/index.js @@ -6,11 +6,11 @@ import propTypes from 'prop-types'; import { captitalizeWords, checkFiltered, + getRulesToFilterList, formatDateTime, formatElapsedMs, formatTime, getBlockingClientName, - getFilterName, getServiceName, processContent, } from '../../../helpers/helpers'; @@ -70,8 +70,7 @@ const Row = memo(({ upstream, type, client_proto, - filterId, - rule, + rules, originalResponse, status, service_name, @@ -107,8 +106,6 @@ const Row = memo(({ const sourceData = getSourceData(tracker); - const filter = getFilterName(filters, whitelistFilters, filterId); - const { confirmMessage, buttonKey: blockingClientKey, @@ -172,8 +169,8 @@ const Row = memo(({ response_details: 'title', install_settings_dns: upstream, elapsed: formattedElapsedMs, - filter: rule ? filter : null, - rule_label: rule, + rule_label: rules.length > 0 + && getRulesToFilterList(rules, filters, whitelistFilters), response_table_header: response?.join('\n'), response_code: status, client_details: 'title', @@ -235,8 +232,10 @@ Row.propTypes = { upstream: propTypes.string.isRequired, type: propTypes.string.isRequired, client_proto: propTypes.string.isRequired, - filterId: propTypes.number, - rule: propTypes.string, + rules: propTypes.arrayOf(propTypes.shape({ + text: propTypes.string.isRequired, + filter_list_id: propTypes.number.isRequired, + })), originalResponse: propTypes.array, status: propTypes.string.isRequired, service_name: propTypes.string, diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 64300bb9..a48a8d15 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -428,3 +428,13 @@ margin-right: 1px; opacity: 0.5; } + +.filteringRules__rule { + margin-bottom: 0; +} + +.filteringRules__filter { + font-style: italic; + font-weight: normal; + margin-bottom: 1rem; +} diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 7d44b400..82f30245 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -7,6 +7,7 @@ import i18n from 'i18next'; import uniqBy from 'lodash/uniqBy'; import ipaddr from 'ipaddr.js'; import queryString from 'query-string'; +import React from 'react'; import { getTrackerData } from './trackers/trackers'; import { @@ -68,6 +69,7 @@ export const normalizeLogs = (logs) => logs.map((log) => { time, filterId, rule, + rules, service_name, original_answer, upstream, @@ -80,6 +82,15 @@ export const normalizeLogs = (logs) => logs.map((log) => { 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 { time, domain, @@ -88,8 +99,10 @@ export const normalizeLogs = (logs) => logs.map((log) => { reason, client, client_proto, + /* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */ filterId, rule, + rules: newRules, status, service_name, originalAnswer: original_answer, @@ -726,6 +739,75 @@ export const getFilterName = ( 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
+ {Object.entries(filterNameToRulesMap).reduce( + (acc, [filterName, rulesArr]) => acc + .concat(rulesArr.map((rule, i) =>
{rule}
)) + .concat(
{filterName}
), + [], + )} +
; +}; + +/** +* @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 gateway_ip {string} diff --git a/client/src/helpers/renderFormattedClientCell.js b/client/src/helpers/renderFormattedClientCell.js index d677c4ca..f7e59a84 100644 --- a/client/src/helpers/renderFormattedClientCell.js +++ b/client/src/helpers/renderFormattedClientCell.js @@ -31,7 +31,7 @@ const getFormattedWhois = (whois) => { * @param {object} info.whois_info * @param {boolean} [isDetailed] * @param {boolean} [isLogs] - * @returns {JSX.Element} + * @returns {JSXElement} */ export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => { let whoisContainer = null;