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}}0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}}0>. 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}}0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}0>. 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-TLS0>",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS0>",
+ "example_upstream_doq": "encrypted <0>DNS-over-QUIC0>",
"example_upstream_sdns": "you can use <0>DNS Stamps0> for <1>DNSCrypt1> or <2>DNS-over-HTTPS2> 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}}0> 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 providers0> 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 instruction0> on how to resolve this."
+ "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction0> 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
{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 &&
+
+
+
+
+
+ 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)}
+
+
+
@@ -45,6 +50,7 @@ Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
+ options: PropTypes.object,
};
export default Toast;
diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css
index cef0e71d..5930d881 100644
--- a/client/src/components/ui/Card.css
+++ b/client/src/components/ui/Card.css
@@ -16,7 +16,11 @@
.card-table-overflow--limited {
overflow-y: auto;
- max-height: 280px;
+ max-height: 17.5rem;
+}
+
+.card-table-overflow--limited.clients__table {
+ max-height: 18rem;
}
.card-actions {
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index f29bcc21..3851ff01 100644
--- a/client/src/components/ui/Icons.js
+++ b/client/src/components/ui/Icons.js
@@ -344,6 +344,14 @@ const Icons = () => (
+
+
+
+
+
+
+
);
diff --git a/client/src/components/ui/Tooltip.js b/client/src/components/ui/Tooltip.js
index 9f34b3fe..87b353de 100644
--- a/client/src/components/ui/Tooltip.js
+++ b/client/src/components/ui/Tooltip.js
@@ -20,6 +20,7 @@ const Tooltip = ({
trigger = 'hover',
delayShow = SHOW_TOOLTIP_DELAY,
delayHide = HIDE_TOOLTIP_DELAY,
+ onVisibilityChange,
}) => {
const { t } = useTranslation();
const touchEventsAvailable = 'ontouchstart' in window;
@@ -73,6 +74,7 @@ const Tooltip = ({
delayHide={delayHideValue}
delayShow={delayShowValue}
tooltip={renderTooltip}
+ onVisibilityChange={onVisibilityChange}
>
{renderTrigger}
@@ -90,10 +92,11 @@ Tooltip.propTypes = {
).isRequired,
placement: propTypes.string,
trigger: propTypes.string,
- delayHide: propTypes.string,
- delayShow: propTypes.string,
+ delayHide: propTypes.number,
+ delayShow: propTypes.number,
className: propTypes.string,
triggerClass: propTypes.string,
+ onVisibilityChange: propTypes.func,
};
export default Tooltip;
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 163b11a6..69450297 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -53,6 +53,8 @@ export const REPOSITORY = {
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';
+export const GETTING_STARTED_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update';
+
export const ADDRESS_IN_USE_TEXT = 'address already in use';
export const INSTALL_FIRST_STEP = 1;
@@ -69,6 +71,7 @@ export const STANDARD_DNS_PORT = 53;
export const STANDARD_WEB_PORT = 80;
export const STANDARD_HTTPS_PORT = 443;
export const DNS_OVER_TLS_PORT = 853;
+export const DNS_OVER_QUIC_PORT = 784;
export const MAX_PORT = 65535;
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
@@ -76,8 +79,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300;
export const DEBOUNCE_FILTER_TIMEOUT = 500;
export const CHECK_TIMEOUT = 1000;
-export const SUCCESS_TOAST_TIMEOUT = 5000;
-export const FAILURE_TOAST_TIMEOUT = 30000;
export const HIDE_TOOLTIP_DELAY = 300;
export const SHOW_TOOLTIP_DELAY = 200;
export const MODAL_OPEN_TIMEOUT = 150;
@@ -541,8 +542,17 @@ export const TOAST_TYPES = {
NOTICE: 'notice',
};
+export const SUCCESS_TOAST_TIMEOUT = 5000;
+export const FAILURE_TOAST_TIMEOUT = 30000;
+
export const TOAST_TIMEOUTS = {
- [TOAST_TYPES.SUCCESS]: 5000,
- [TOAST_TYPES.ERROR]: 30000,
- [TOAST_TYPES.NOTICE]: 30000,
+ [TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,
+ [TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,
+ [TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,
+};
+
+export const ADDRESS_TYPES = {
+ IP: 'IP',
+ CIDR: 'CIDR',
+ UNKNOWN: 'UNKNOWN',
};
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index 9fdd9fad..f509ef44 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -14,6 +14,7 @@ import queryString from 'query-string';
import { getTrackerData } from './trackers/trackers';
import {
+ ADDRESS_TYPES,
CHECK_TIMEOUT,
CUSTOM_FILTERING_RULES_ID,
DEFAULT_DATE_FORMAT_OPTIONS,
@@ -97,7 +98,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
filterId,
rule,
status,
- serviceName: service_name,
+ service_name,
originalAnswer: original_answer,
originalResponse: processResponse(original_answer),
tracker: getTrackerData(domain),
@@ -509,6 +510,18 @@ const isIpMatchCidr = (parsedIp, parsedCidr) => {
}
};
+export const isIpInCidr = (ip, cidr) => {
+ try {
+ const parsedIp = ipaddr.parse(ip);
+ const parsedCidr = ipaddr.parseCIDR(cidr);
+
+ return isIpMatchCidr(parsedIp, parsedCidr);
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+};
+
/**
* The purpose of this method is to quickly check
* if this IP can possibly be in the specified CIDR range.
@@ -578,6 +591,29 @@ const isIpQuickMatchCIDR = (ip, listItem) => {
return false;
};
+/**
+ *
+ * @param ipOrCidr
+ * @returns {'IP' | 'CIDR' | 'UNKNOWN'}
+ *
+ */
+export const findAddressType = (address) => {
+ try {
+ const cidrMaybe = address.includes('/');
+
+ if (!cidrMaybe && ipaddr.isValid(address)) {
+ return ADDRESS_TYPES.IP;
+ }
+ if (cidrMaybe && ipaddr.parseCIDR(address)) {
+ return ADDRESS_TYPES.CIDR;
+ }
+
+ return ADDRESS_TYPES.UNKNOWN;
+ } catch (e) {
+ return ADDRESS_TYPES.UNKNOWN;
+ }
+};
+
/**
* @param ip {string}
* @param list {string}
@@ -622,6 +658,42 @@ export const getIpMatchListStatus = (ip, list) => {
}
};
+/**
+ * @param ids {string[]}
+ * @returns {Object}
+ */
+export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => {
+ const addressType = findAddressType(curr);
+
+ if (addressType === ADDRESS_TYPES.IP) {
+ acc.ips.push(curr);
+ }
+ if (addressType === ADDRESS_TYPES.CIDR) {
+ acc.cidrs.push(curr);
+ }
+ return acc;
+}, { ips: [], cidrs: [] });
+
+export const countClientsStatistics = (ids, autoClients) => {
+ const { ips, cidrs } = separateIpsAndCidrs(ids);
+
+ const ipsCount = ips.reduce((acc, curr) => {
+ const count = autoClients[curr] || 0;
+ return acc + count;
+ }, 0);
+
+ const cidrsCount = Object.entries(autoClients)
+ .reduce((acc, curr) => {
+ const [id, count] = curr;
+ if (cidrs.some((cidr) => isIpInCidr(id, cidr))) {
+ // eslint-disable-next-line no-param-reassign
+ acc += count;
+ }
+ return acc;
+ }, 0);
+
+ return ipsCount + cidrsCount;
+};
/**
* @param {string} elapsedMs
@@ -836,3 +908,21 @@ export const isScrolledIntoView = (el) => {
return elemTop < window.innerHeight && elemBottom >= 0;
};
+
+/**
+ * If this is a manually created client, return its name.
+ * If this is a "runtime" client, return it's IP address.
+ * @param clients {Array.