diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d933e462..4fd4e4f5 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,6 +1,10 @@ #!/bin/bash set -e; -git diff --cached --name-only | grep -q '.js$' && make lint-js; +git diff --cached --name-only | grep -q '.js$' && found=1 +if [ $found == 1 ]; then + make lint-js || exit 1 + npm run test --prefix client || exit 1 +fi found=0 git diff --cached --name-only | grep -q '.go$' && found=1 diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 49b62d52..469c1c79 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -743,6 +743,7 @@ Response: "server_name":"...", "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", @@ -774,6 +775,7 @@ Request: "force_https":false, "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", // if set, certificate_chain must be empty diff --git a/client/package-lock.json b/client/package-lock.json index 84c8e186..42cac10f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12377,9 +12377,9 @@ } }, "react-i18next": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz", - "integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==", + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz", + "integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==", "requires": { "@babel/runtime": "^7.3.1", "html-parse-stringify2": "2.0.1" diff --git a/client/package.json b/client/package.json index fdc19c9d..4ad8d5eb 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,7 @@ "react": "^16.13.1", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", - "react-i18next": "^11.4.0", + "react-i18next": "^11.7.2", "react-modal": "^3.11.2", "react-popper-tooltip": "^2.11.1", "react-redux": "^7.2.0", diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json index 2a9784ad..72f7989f 100644 --- a/client/src/__locales/de.json +++ b/client/src/__locales/de.json @@ -45,7 +45,7 @@ "dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!", "dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.", "dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.", - "dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}}. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.", + "dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.", "dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt", "dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt", "dhcp_new_static_lease": "Neuer statischer Lease", @@ -99,7 +99,7 @@ "no_clients_found": "Keine Clients gefunden", "general_statistics": "Allgemeine Statistiken", "number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen", - "number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}}} Tagen verarbeitet wurden", + "number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden", "number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen", "number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen", "number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index fe21ef6c..791bb301 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -186,6 +186,7 @@ "example_upstream_regular": "regular DNS (over UDP)", "example_upstream_dot": "encrypted <0>DNS-over-TLS", "example_upstream_doh": "encrypted <0>DNS-over-HTTPS", + "example_upstream_doq": "encrypted <0>DNS-over-QUIC", "example_upstream_sdns": "you can use <0>DNS Stamps for <1>DNSCrypt or <2>DNS-over-HTTPS resolvers", "example_upstream_tcp": "regular DNS (over TCP)", "all_lists_up_to_date_toast": "All lists are already up-to-date", @@ -194,6 +195,10 @@ "dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly", "unblock": "Unblock", "block": "Block", + "disallow_this_client": "Disallow this client", + "allow_this_client": "Allow this client", + "block_for_this_client_only": "Block for this client only", + "unblock_for_this_client_only": "Unblock for this client only", "time_table_header": "Time", "date": "Date", "domain_name_table_header": "Domain name", @@ -327,6 +332,8 @@ "encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.", "encryption_dot": "DNS-over-TLS port", "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.", + "encryption_doq": "DNS-over-QUIC port", + "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable. Also, there are not too many clients that support it at the moment.", "encryption_certificates": "Certificates", "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}} or you can buy it from one of the trusted Certificate Authorities.", "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.", @@ -360,7 +367,7 @@ "fix": "Fix", "dns_providers": "Here is a <0>list of known DNS providers to choose from.", "update_now": "Update now", - "update_failed": "Auto-update failed. Please follow the steps to update manually.", + "update_failed": "Auto-update failed. Please follow these steps to update manually.", "processing_update": "Please wait, AdGuard Home is being updated", "clients_title": "Clients", "clients_desc": "Configure devices connected to AdGuard Home", @@ -570,5 +577,7 @@ "setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server", "original_response": "Original response", "click_to_view_queries": "Click to view queries", - "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this." + "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this.", + "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.", + "experimental": "Experimental" } diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js index f974cca6..bb371be4 100644 --- a/client/src/__tests__/helpers.test.js +++ b/client/src/__tests__/helpers.test.js @@ -1,5 +1,7 @@ -import { getIpMatchListStatus, sortIp } from '../helpers/helpers'; -import { IP_MATCH_LIST_STATUS } from '../helpers/constants'; +import { + countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp, +} from '../helpers/helpers'; +import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants'; describe('getIpMatchListStatus', () => { describe('IPv4', () => { @@ -482,3 +484,56 @@ describe('sortIp', () => { }); }); }); + +describe('findAddressType', () => { + describe('ip', () => { + expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP); + }); + describe('cidr', () => { + expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR); + }); + describe('mac', () => { + expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN); + }); +}); + +describe('countClientsStatistics', () => { + test('single ip', () => { + expect(countClientsStatistics(['127.0.0.1'], { + '127.0.0.1': 1, + })).toStrictEqual(1); + }); + test('multiple ip', () => { + expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr', () => { + expect(countClientsStatistics(['127.0.0.0/8'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr and multiple ip', () => { + expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(1 + 2 + 3); + }); + test('mac', () => { + expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(2 + 3); + }); + test('not found', () => { + expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(0); + }); +}); diff --git a/client/src/actions/encryption.js b/client/src/actions/encryption.js index 0e743323..36faf2ec 100644 --- a/client/src/actions/encryption.js +++ b/client/src/actions/encryption.js @@ -34,6 +34,7 @@ export const setTlsConfig = (config) => async (dispatch, getState) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.setTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); @@ -59,6 +60,7 @@ export const validateTlsConfig = (config) => async (dispatch) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.validateTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index ff512883..ff039af1 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -4,9 +4,10 @@ import axios from 'axios'; import endsWith from 'lodash/endsWith'; import escapeRegExp from 'lodash/escapeRegExp'; +import React from 'react'; import { splitByNewLine, sortClients } from '../helpers/helpers'; import { - BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, + BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK, } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; @@ -184,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => { dispatch(getUpdateRequest()); const handleRequestError = () => { - dispatch(addNoticeToast({ error: 'update_failed' })); + const options = { + components: { + a: , + }, + }; + + dispatch(addNoticeToast({ error: 'update_failed', options })); dispatch(getUpdateFailure()); }; @@ -545,15 +553,17 @@ export const removeStaticLease = (config) => async (dispatch) => { export const removeToast = createAction('REMOVE_TOAST'); -export const toggleBlocking = (type, domain) => async (dispatch, getState) => { +export const toggleBlocking = ( + type, domain, baseRule, baseUnblocking, +) => async (dispatch, getState) => { + const baseBlockingRule = baseRule || `||${domain}^$important`; + const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`; const { userRules } = getState().filtering; const lineEnding = !endsWith(userRules, '\n') ? '\n' : ''; - const baseRule = `||${domain}^$important`; - const baseUnblocking = `@@${baseRule}`; - const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule; - const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking; + const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule; + const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule; const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`); const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`); @@ -576,3 +586,10 @@ export const toggleBlocking = (type, domain) => async (dispatch, getState) => { dispatch(getFilteringStatus()); }; + +export const toggleBlockingForClient = (type, domain, client) => { + const baseRule = `||${domain}^$client='${client.replace(/'/g, '/\'')}'`; + const baseUnblocking = `@@${baseRule}`; + + return toggleBlocking(type, domain, baseRule, baseUnblocking); +}; diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css index 091a6612..e2b0304d 100644 --- a/client/src/components/App/index.css +++ b/client/src/components/App/index.css @@ -66,3 +66,12 @@ body { .select--no-warning { margin-bottom: 1.375rem; } + +.button-action { + visibility: hidden; +} + +.logs__row:hover .button-action, +.button-action--active { + visibility: visible; +} diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index 24b278f4..3c163035 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -51,15 +51,16 @@ const renderBlockingButton = (ip) => { const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK; const text = type; - const className = classNames('btn btn-sm', { - 'btn-outline-danger': isNotFound, - 'btn-outline-secondary': !isNotFound, + const buttonClass = classNames('button-action button-action--main', { + 'button-action--unblock': !isNotFound, }); const toggleClientStatus = (type, ip) => { - const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock'; + const confirmMessage = type === BLOCK_ACTIONS.BLOCK + ? `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}` + : t('client_confirm_unblock', { ip }); - if (window.confirm(t(confirmMessage, { ip }))) { + if (window.confirm(confirmMessage)) { dispatch(toggleClientBlock(type, ip)); } }; @@ -69,7 +70,7 @@ const renderBlockingButton = (ip) => { return
; + const getOptions = (optionToActionMap) => { + const options = Object.entries(optionToActionMap); + if (options.length === 0) { + return null; + } + return <>{options + .map(([name, onClick]) =>
{t(name)} +
)}; + }; + + const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP); + + const buttonClass = classNames('button-action button-action--main', { + 'button-action--unblock': isFiltered, + 'button-action--with-options': content, + 'button-action--active': isOptionsOpened, + }); + + const buttonArrowClass = classNames('button-action button-action--arrow', { + 'button-action--unblock': isFiltered, + 'button-action--active': isOptionsOpened, + }); + + const containerClass = classNames('button-action__container', { + 'button-action__container--detailed': isDetailed, + }); + + return
+ + {content && } +
; }; return
@@ -81,9 +148,7 @@ const ClientCell = ({
{isDetailed && name && !whoisAvailable &&
- {name} -
} + title={name}>{name}
} {renderBlockingButton(isFiltered, domain)} ; diff --git a/client/src/components/Logs/Cells/DomainCell.js b/client/src/components/Logs/Cells/DomainCell.js index 4333089c..47d14846 100644 --- a/client/src/components/Logs/Cells/DomainCell.js +++ b/client/src/components/Logs/Cells/DomainCell.js @@ -14,6 +14,7 @@ import IconTooltip from './IconTooltip'; const DomainCell = ({ answer_dnssec, + service_name, client_proto, domain, time, @@ -49,6 +50,10 @@ const DomainCell = ({ protocol, }; + if (service_name) { + requestDetailsObj.check_service = service_name; + } + const sourceData = getSourceData(tracker); const knownTrackerDataObj = { @@ -98,7 +103,7 @@ const DomainCell = ({ xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent} place='bottom' />
-
{domain}
+
{service_name || domain}
{details && isDetailed &&
{details}
} @@ -112,6 +117,7 @@ DomainCell.propTypes = { domain: propTypes.string.isRequired, time: propTypes.string.isRequired, type: propTypes.string.isRequired, + service_name: propTypes.string, tracker: propTypes.object, }; diff --git a/client/src/components/Logs/Cells/IconTooltip.js b/client/src/components/Logs/Cells/IconTooltip.js index 5b9cc2cb..8bb3d624 100644 --- a/client/src/components/Logs/Cells/IconTooltip.js +++ b/client/src/components/Logs/Cells/IconTooltip.js @@ -6,17 +6,21 @@ import { processContent } from '../../../helpers/helpers'; import Tooltip from '../../ui/Tooltip'; import 'react-popper-tooltip/dist/styles.css'; import './IconTooltip.css'; +import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants'; const IconTooltip = ({ className, contentItemClass, columnClass, + triggerClass, canShowTooltip = true, xlinkHref, title, placement, tooltipClass, content, + trigger, + onVisibilityChange, renderContent = content ? React.Children.map( processContent(content), (item, idx) =>
@@ -36,6 +40,10 @@ const IconTooltip = ({ className={tooltipClassName} content={tooltipContent} placement={placement} + triggerClass={triggerClass} + trigger={trigger} + onVisibilityChange={onVisibilityChange} + delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY} > {xlinkHref && @@ -45,6 +53,8 @@ const IconTooltip = ({ IconTooltip.propTypes = { className: PropTypes.string, + trigger: PropTypes.string, + triggerClass: PropTypes.string, contentItemClass: PropTypes.string, columnClass: PropTypes.string, tooltipClass: PropTypes.string, @@ -52,11 +62,9 @@ IconTooltip.propTypes = { placement: PropTypes.string, canShowTooltip: PropTypes.bool, xlinkHref: PropTypes.string, - content: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array, - ]), + content: PropTypes.node, renderContent: PropTypes.arrayOf(PropTypes.element), + onVisibilityChange: PropTypes.func, }; export default IconTooltip; diff --git a/client/src/components/Logs/Cells/helpers/index.js b/client/src/components/Logs/Cells/helpers/index.js new file mode 100644 index 00000000..61e7ff5c --- /dev/null +++ b/client/src/components/Logs/Cells/helpers/index.js @@ -0,0 +1,19 @@ +import { getIpMatchListStatus } from '../../../../helpers/helpers'; +import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants'; + +export const BUTTON_PREFIX = 'btn_'; + +export const getBlockClientInfo = (client, disallowed_clients) => { + const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients); + + const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND; + const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK; + + const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock'; + const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client'; + return { + confirmMessage, + buttonKey, + type, + }; +}; diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js index c2d7968b..8a0fced3 100644 --- a/client/src/components/Logs/Cells/index.js +++ b/client/src/components/Logs/Cells/index.js @@ -9,6 +9,7 @@ import { formatDateTime, formatElapsedMs, formatTime, + getBlockingClientName, getFilterName, processContent, } from '../../../helpers/helpers'; @@ -22,12 +23,14 @@ import { SCHEME_TO_PROTOCOL_MAP, } from '../../../helpers/constants'; import { getSourceData } from '../../../helpers/trackers/trackers'; -import { toggleBlocking } from '../../../actions'; +import { toggleBlocking, toggleBlockingForClient } from '../../../actions'; import DateCell from './DateCell'; import DomainCell from './DomainCell'; import ResponseCell from './ResponseCell'; import ClientCell from './ClientCell'; import '../Logs.css'; +import { toggleClientBlock } from '../../../actions/access'; +import { getBlockClientInfo, BUTTON_PREFIX } from './helpers'; const Row = memo(({ style, @@ -45,6 +48,13 @@ const Row = memo(({ const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual); + const disallowed_clients = useSelector( + (state) => state.access.disallowed_clients, + shallowEqual, + ); + + const clients = useSelector((state) => state.dashboard.clients); + const onClick = () => { if (!isSmallScreen) { return; } const { @@ -98,6 +108,26 @@ const Row = memo(({ const filter = getFilterName(filters, whitelistFilters, filterId); + const { + confirmMessage, + buttonKey: blockingClientKey, + type: blockType, + } = getBlockClientInfo(client, disallowed_clients); + + const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; + const clientNameBlockingFor = getBlockingClientName(clients, client); + + const onBlockingForClientClick = () => { + dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor)); + }; + + const onBlockingClientClick = () => { + const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`; + if (window.confirm(message)) { + dispatch(toggleClientBlock(blockType, client)); + } + }; + const detailedData = { time_table_header: formatTime(time, LONG_TIME_FORMAT), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), @@ -132,10 +162,12 @@ const Row = memo(({ source_label: source, validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false, original_response: originalResponse?.join('\n'), - [buttonType]:
{t(buttonType)}
, + [BUTTON_PREFIX + buttonType]:
{t(buttonType)}
, + [BUTTON_PREFIX + blockingForClientKey]:
{t(blockingForClientKey)}
, + [BUTTON_PREFIX + blockingClientKey]:
{t(blockingClientKey)}
, }; setDetailedDataCurrent(processContent(detailedData)); diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 857fd466..b230ab8b 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -10,9 +10,20 @@ --size-client: 123; --gray-216: rgba(216, 216, 216, 0.23); --gray-4d: #4D4D4D; + --gray-f3: #F3F3F3; --gray-8: #888; --danger: #DF3812; --white80: rgba(255, 255, 255, 0.8); + + --btn-block: #C23814; + --btn-block-disabled: #E3B3A6; + --btn-block-active: #A62200; + + --btn-unblock: #888888; + --btn-unblock-disabled: #D8D8D8; + --btn-unblock-active: #4D4D4D; + + --option-border-radius: 4px; } .logs__text { @@ -191,6 +202,7 @@ width: 7.6875rem; flex: var(--size-client) 0 auto; padding-right: 0; + position: relative; } .logs__cell--header__container > .logs__cell--header__item { @@ -202,12 +214,95 @@ padding-right: 0; } -.logs__cell--block-button { - max-height: 1.75rem; - position: relative; - left: 10%; - top: 40%; - visibility: hidden; +.button-action__container { + display: flex; + position: absolute; + right: 0; + bottom: 0.5rem; + height: 1.6rem; +} + +.button-action__container--detailed { + bottom: 1.3rem; +} + +.button-action { + outline: 0 !important; + background: var(--btn-block); + border-radius: var(--option-border-radius); + font-size: 0.8rem; + color: var(--white); + letter-spacing: 0; + text-align: center; + line-height: 28px; + border: 0; +} + +.button-action--unblock { + background: var(--btn-unblock); +} + +.button-action--main { + padding: 0 1rem; + display: flex; + align-items: center; +} + +.button-action--with-options { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.button-action--arrow { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid var(--white); + width: 1.5625rem; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.button-action:hover { + cursor: pointer; +} + +.button-action--arrow .button-action--icon { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.button-action:active { + background: var(--btn-block-active); +} + +.button-action--unblock:active { + background: var(--btn-unblock-active); +} + +.button-action:disabled { + background: var(--btn-block-disabled); + cursor: default; +} + +.button-action--unblock:disabled { + background: var(--btn-unblock-disabled); +} + +.button-action--arrow-option:hover { + cursor: pointer; + background: var(--gray-f3); + overflow: hidden; +} + +.button-action--arrow-option-container { + overflow: visible; + transform-origin: left; + padding: 1rem 0; } .logs__row { @@ -222,14 +317,6 @@ border-bottom: 2px solid var(--gray-216); } -.logs__table .logs__row:hover .logs__cell--block-button { - visibility: visible; -} - -.logs__table .logs__row .logs__cell--block-button:disabled { - background-color: var(--white) !important; -} - /* QUERY_STATUS_COLORS */ .logs__row--blue { background-color: var(--blue); @@ -301,3 +388,28 @@ .logs__table .loading:before { min-height: 100%; } + +.logs__whois { + display: inline; + font-size: 12px; + white-space: nowrap; +} + +.logs__whois::after { + content: "|"; + padding: 0 5px; + opacity: 0.3; +} + +.logs__whois:last-child::after { + content: ""; +} + +.logs__whois-icon.icons { + position: relative; + top: -2px; + width: 12px; + height: 12px; + margin-right: 1px; + opacity: 0.5; +} diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 13fa697c..bcc9a94d 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -23,15 +23,16 @@ import { } from '../../actions/queryLogs'; import InfiniteTable from './InfiniteTable'; import './Logs.css'; +import { BUTTON_PREFIX } from './Cells/helpers'; -const processContent = (data, buttonType) => Object.entries(data) +const processContent = (data) => Object.entries(data) .map(([key, value]) => { if (!value) { return null; } const isTitle = value === 'title'; - const isButton = key === buttonType; + const isButton = key.startsWith(BUTTON_PREFIX); const isBoolean = typeof value === 'boolean'; const isHidden = isBoolean && value === false; diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index cb7f2914..ac613164 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next'; import ReactTable from 'react-table'; import { MODAL_TYPE } from '../../../helpers/constants'; -import { splitByNewLine } from '../../../helpers/helpers'; +import { splitByNewLine, countClientsStatistics } from '../../../helpers/helpers'; import Card from '../../ui/Card'; import Modal from './Modal'; import CellWrap from '../../ui/CellWrap'; @@ -204,7 +204,10 @@ class ClientsTable extends Component { { Header: this.props.t('requests_count'), id: 'statistics', - accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0, + accessor: (row) => countClientsStatistics( + row.ids, + this.props.normalizedTopClients.auto, + ), sortMethod: (a, b) => b - a, minWidth: 120, Cell: (row) => { diff --git a/client/src/components/Settings/Clients/whoisCell.js b/client/src/components/Settings/Clients/whoisCell.js index 94afd6cc..1a8b0484 100644 --- a/client/src/components/Settings/Clients/whoisCell.js +++ b/client/src/components/Settings/Clients/whoisCell.js @@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
{icon && ( - +   diff --git a/client/src/components/Settings/Dns/Upstream/Examples.js b/client/src/components/Settings/Dns/Upstream/Examples.js index de779b18..70797909 100644 --- a/client/src/components/Settings/Dns/Upstream/Examples.js +++ b/client/src/components/Settings/Dns/Upstream/Examples.js @@ -63,6 +63,27 @@ const Examples = (props) => ( +
  • + quic://dns-unfiltered.adguard.com:784 –  + + + DNS-over-QUIC + , + ]} + > + example_upstream_doq + +   + (experimental) + +
  • tcp://9.9.9.9example_upstream_tcp
  • diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index 7be23b10..15f8a3c6 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -11,11 +11,15 @@ import { renderRadioField, toNumber, } from '../../../helpers/form'; -import { validateIsSafePort, validatePort, validatePortTLS } from '../../../helpers/validators'; +import { + validateIsSafePort, validatePort, validatePortQuic, validatePortTLS, +} from '../../../helpers/validators'; import i18n from '../../../i18n'; import KeyStatus from './KeyStatus'; import CertificateStatus from './CertificateStatus'; -import { DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT } from '../../../helpers/constants'; +import { + DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT, +} from '../../../helpers/constants'; const validate = (values) => { const errors = {}; @@ -38,6 +42,7 @@ const clearFields = (change, setTlsConfig, t) => { certificate_path: '', port_https: STANDARD_HTTPS_PORT, port_dns_over_tls: DNS_OVER_TLS_PORT, + port_dns_over_quic: DNS_OVER_QUIC_PORT, server_name: '', force_https: false, enabled: false, @@ -189,6 +194,30 @@ let Form = (props) => {
    +
    +
    + + +
    + encryption_doq_desc +
    +
    +
    diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js index 7c2cccc8..f7ca52e0 100644 --- a/client/src/components/Settings/Encryption/index.js +++ b/client/src/components/Settings/Encryption/index.js @@ -66,6 +66,7 @@ class Encryption extends Component { force_https, port_https, port_dns_over_tls, + port_dns_over_quic, certificate_chain, private_key, certificate_path, @@ -78,6 +79,7 @@ class Encryption extends Component { force_https, port_https, port_dns_over_tls, + port_dns_over_quic, certificate_chain, private_key, certificate_path, diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 3bf1a121..4efb0868 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -54,7 +54,7 @@ } .form__message--error { - color: var(--red); + color: #cd201f; } .form__message--left-pad { diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js index a4c58aad..4c46078a 100644 --- a/client/src/components/Toasts/Toast.js +++ b/client/src/components/Toasts/Toast.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { useTranslation } from 'react-i18next'; +import { Trans } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { TOAST_TIMEOUTS } from '../../helpers/constants'; import { removeToast } from '../../actions'; @@ -9,8 +9,8 @@ const Toast = ({ id, message, type, + options, }) => { - const { t } = useTranslation(); const dispatch = useDispatch(); const [timerId, setTimerId] = useState(null); @@ -30,7 +30,12 @@ const Toast = ({ return
    -

    {t(message)}

    +

    + +