Merge branch 'master' into 1914-refused-blocking-mode
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
"example_upstream_regular": "regular DNS (over UDP)",
|
||||
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>",
|
||||
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>",
|
||||
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>",
|
||||
"example_upstream_sdns": "you can use <0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> 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 providers</0> to choose from.",
|
||||
"update_now": "Update now",
|
||||
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps</a> to update manually.",
|
||||
"update_failed": "Auto-update failed. Please <a>follow these steps</a> 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</0> 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</0> on how to resolve this.",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.",
|
||||
"experimental": "Experimental"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: <a href={GETTING_STARTED_LINK} target="_blank"
|
||||
rel="noopener noreferrer" />,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <div className="table__action pl-4">
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingSet}
|
||||
>
|
||||
@@ -82,7 +83,7 @@ const ClientCell = (row) => {
|
||||
const { value, original: { info } } = row;
|
||||
|
||||
return <>
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex">
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
|
||||
{renderFormattedClientCell(value, info, true)}
|
||||
{renderBlockingButton(value)}
|
||||
</div>
|
||||
|
||||
@@ -164,6 +164,10 @@
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.nav-icon--white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-brand-img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import { checkFiltered } from '../../../helpers/helpers';
|
||||
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||
import IconTooltip from './IconTooltip';
|
||||
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||
import { toggleClientBlock } from '../../../actions/access';
|
||||
import { getBlockClientInfo } from './helpers';
|
||||
|
||||
const ClientCell = ({
|
||||
client,
|
||||
@@ -22,6 +24,12 @@ const ClientCell = ({
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
const [isOptionsOpened, setOptionsOpened] = useState(false);
|
||||
|
||||
const disallowed_clients = useSelector(
|
||||
(state) => state.access.disallowed_clients,
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
@@ -53,22 +61,81 @@ const ClientCell = ({
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
const clients = useSelector((state) => state.dashboard.clients);
|
||||
|
||||
const buttonClass = classNames('btn btn-sm logs__cell--block-button', {
|
||||
'btn-outline-secondary': isFiltered,
|
||||
'btn-outline-danger': !isFiltered,
|
||||
});
|
||||
const {
|
||||
confirmMessage,
|
||||
buttonKey: blockingClientKey,
|
||||
type,
|
||||
} = getBlockClientInfo(client, disallowed_clients);
|
||||
|
||||
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
|
||||
const clientNameBlockingFor = getBlockingClientName(clients, client);
|
||||
|
||||
const BUTTON_OPTIONS_TO_ACTION_MAP = {
|
||||
[blockingForClientKey]: () => {
|
||||
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
||||
},
|
||||
[blockingClientKey]: () => {
|
||||
const message = `${type === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
|
||||
if (window.confirm(message)) {
|
||||
dispatch(toggleClientBlock(type, client));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const onClick = () => dispatch(toggleBlocking(buttonType, domain));
|
||||
|
||||
return <button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>;
|
||||
const getOptions = (optionToActionMap) => {
|
||||
const options = Object.entries(optionToActionMap);
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return <>{options
|
||||
.map(([name, onClick]) => <div
|
||||
key={name}
|
||||
className="button-action--arrow-option px-4 py-2"
|
||||
onClick={onClick}
|
||||
>{t(name)}
|
||||
</div>)}</>;
|
||||
};
|
||||
|
||||
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 <div className={containerClass}>
|
||||
<button type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>
|
||||
{content && <button className={buttonArrowClass} disabled={processingRules}>
|
||||
<IconTooltip
|
||||
className='h-100'
|
||||
tooltipClass='button-action--arrow-option-container'
|
||||
xlinkHref='chevron-down'
|
||||
triggerClass='button-action--icon'
|
||||
content={content} placement="bottom-end" trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
/>
|
||||
</button>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
|
||||
@@ -81,9 +148,7 @@ const ClientCell = ({
|
||||
</div>
|
||||
{isDetailed && name && !whoisAvailable
|
||||
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>
|
||||
{name}
|
||||
</div>}
|
||||
title={name}>{name}</div>}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>;
|
||||
|
||||
@@ -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' />
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
<div className="text-truncate" title={domain}>{service_name || domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => <div key={idx} className={contentItemClass}>
|
||||
@@ -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 && <svg className={className}>
|
||||
<use xlinkHref={`#${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;
|
||||
|
||||
19
client/src/components/Logs/Cells/helpers/index.js
Normal file
19
client/src/components/Logs/Cells/helpers/index.js
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
[BUTTON_PREFIX + buttonType]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
[BUTTON_PREFIX + blockingForClientKey]: <div onClick={onBlockingForClientClick} className='text-center font-weight-bold py-2'>{t(blockingForClientKey)}</div>,
|
||||
[BUTTON_PREFIX + blockingClientKey]: <div onClick={onBlockingClientClick} className='text-center font-weight-bold py-2'>{t(blockingClientKey)}</div>,
|
||||
};
|
||||
|
||||
setDetailedDataCurrent(processContent(detailedData));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
|
||||
<div key={key} title={t(key)}>
|
||||
{icon && (
|
||||
<Fragment>
|
||||
<svg className="logs__whois-icon text-muted-dark icons">
|
||||
<svg className="logs__whois-icon text-muted-dark icons icon--24">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
|
||||
|
||||
@@ -63,6 +63,27 @@ const Examples = (props) => (
|
||||
</Trans>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<code>quic://dns-unfiltered.adguard.com:784</code> –
|
||||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a
|
||||
href="https://tools.ietf.org/html/draft-huitema-quic-dnsoquic-07"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS-over-QUIC
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
example_upstream_doq
|
||||
</Trans>
|
||||
|
||||
<span className="text-lowercase">(<Trans>experimental</Trans>)</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<code>tcp://9.9.9.9</code> – <Trans>example_upstream_tcp</Trans>
|
||||
</li>
|
||||
|
||||
@@ -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) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label" htmlFor="port_dns_over_quic">
|
||||
<Trans>encryption_doq</Trans>
|
||||
|
||||
<span className="text-lowercase">(<Trans>experimental</Trans>)</span>
|
||||
</label>
|
||||
<Field
|
||||
id="port_dns_over_quic"
|
||||
name="port_dns_over_quic"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_doq')}
|
||||
validate={[validatePortQuic]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_doq_desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
.form__message--error {
|
||||
color: var(--red);
|
||||
color: #cd201f;
|
||||
}
|
||||
|
||||
.form__message--left-pad {
|
||||
|
||||
@@ -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 <div className={`toast toast--${type}`}
|
||||
onMouseOver={clearRemoveToastTimeout}
|
||||
onMouseOut={setRemoveToastTimeout}>
|
||||
<p className="toast__content">{t(message)}</p>
|
||||
<p className="toast__content">
|
||||
<Trans
|
||||
i18nKey={message}
|
||||
{...options}
|
||||
/>
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={removeCurrentToast}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -45,6 +50,7 @@ Toast.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
options: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -344,6 +344,14 @@ const Icons = () => (
|
||||
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
|
||||
<symbol id="chevron-down" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0h24v24H0z" fill="#878787" fillOpacity=".01" />
|
||||
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
|
||||
d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
||||
</g>
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
</TooltipTrigger>
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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.<object>}
|
||||
* @param ip {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getBlockingClientName = (clients, ip) => {
|
||||
for (let i = 0; i < clients.length; i += 1) {
|
||||
const client = clients[i];
|
||||
|
||||
if (client.ids.includes(ip)) {
|
||||
return client.name;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const getFormattedWhois = (whois) => {
|
||||
.map((key) => {
|
||||
const icon = WHOIS_ICONS[key];
|
||||
return (
|
||||
<span className="logs__whois text-muted " key={key} title={whoisInfo[key]}>
|
||||
<span className="logs__whois text-muted" key={key} title={whoisInfo[key]}>
|
||||
{icon && (
|
||||
<>
|
||||
<svg className="logs__whois-icon icons icon--18">
|
||||
|
||||
@@ -180,6 +180,12 @@ export const validatePortTLS = (value) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {number}
|
||||
* @returns {undefined|string}
|
||||
*/
|
||||
export const validatePortQuic = validatePortTLS;
|
||||
|
||||
/**
|
||||
* @param value {number}
|
||||
* @returns {undefined|string}
|
||||
|
||||
@@ -15,6 +15,7 @@ const toasts = handleActions({
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message,
|
||||
options: payload.options,
|
||||
type: TOAST_TYPES.ERROR,
|
||||
};
|
||||
|
||||
@@ -35,6 +36,7 @@ const toasts = handleActions({
|
||||
const noticeToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
options: payload.options,
|
||||
type: TOAST_TYPES.NOTICE,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user