diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index e8e984e2..6cce0d91 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -194,6 +194,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",
@@ -569,5 +573,6 @@
"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."
}
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index ff512883..d4018bb0 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -545,15 +545,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 +578,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/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 &&
+
+
+
+
+
+
+
);
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/helpers.js b/client/src/helpers/helpers.js
index fa3a5046..bafd230e 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -836,3 +836,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.