mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-26 06:55:48 +03:00
+ client: Redesign query logs block/unblock buttons
Close #2050 Squashed commit of the following: commit 3bc6a409034989b914306e1c33da274730ca623e Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Sep 4 20:58:09 2020 +0300 Change dashboard block confirm message commit d4d47c3557e2166ee04db25a71b782bfbfe3b865 Merge: e8865827fc43e2ac
Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Sep 4 14:56:34 2020 +0300 Merge branch 'master' into feature/2050 commit e8865827879955b1ef62c9ff85798d07bfa4627d Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Sep 4 13:46:10 2020 +0300 Rename classname commit 648151c54e493c63622e014cb9cd1cb450f25478 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Sep 3 19:09:21 2020 +0300 Decrease arrow size commit 4feadab707c613d31225dfa9443a9a836db37ba1 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Sep 3 18:27:41 2020 +0300 Rename button class commit c3919d8ae8d1431657ce61afad2c20e5806f279a Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Sep 3 10:35:15 2020 +0300 Review changes: extract variables commit 0ac809584c391e41a1749a844bc1075e05a92345 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Sep 3 10:13:57 2020 +0300 Display dashboard button on hover commit 1395287c2383e2248a2a5d39451403bd73141e55 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 21:24:04 2020 +0300 Do not hide button on option open commit 947f254b7aea26f289b66b66fac46dba11ea3952 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 21:20:19 2020 +0300 Add buttons for mobile screen commit df05697f87163a2b716d82653884e631f2fa6cf3 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 20:18:20 2020 +0300 Change dashboard button styles commit 16655f2d6b0d79d1fa027ec2310bb0268fffaf6a Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 20:04:28 2020 +0300 Change button styles, rename button options commit 1ac22e875d8b26c16830bf6edb85dadcc19ff287 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 19:30:16 2020 +0300 Review changes commit c590119875439d85927bdd334658e003bc1f0563 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 17:58:08 2020 +0300 Remove default query logs form values commit 141329563417f5337f5659d5500f4cbe16d64bd2 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 17:41:23 2020 +0300 Update blocking buttons options logic, fix button svg size commit 9e4f39aa6cb8e134d80d496b8a248b2fe6aceb99 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Sep 2 16:30:48 2020 +0300 Fix button position commit 8aabff7daccb87ae02c2302e62e296b3cfc17608 Merge: 415a03346b614295
Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Sep 1 17:29:55 2020 +0300 Merge branch 'master' into feature/2050 commit 415a0334561733d92a0f7badd68101ef554dc689 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Sep 1 17:05:51 2020 +0300 Add blocking options commit bc6aed92b6e12f27c2604501275b53bb8159d5bc Merge: 0de4fb3a 40b74522 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Sep 1 15:49:06 2020 +0300 Merge branch 'feature/infinite_scroll_query_logs' into feature/2050 commit 40b745225112cf8d664220ed8f484b0aa16e997c Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Sep 1 15:46:27 2020 +0300 Remove dynamic translation of toasts commit 0de4fb3a4cd785c6b52e860e204c6e13d356b178 Merge: 1ab14471 f08fa7b8 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Sep 1 15:07:30 2020 +0300 Merge branch 'feature/infinite_scroll_query_logs' into feature/2050 ... and 51 more commits
This commit is contained in:
parent
fc43e2ac6f
commit
8dc0108868
14 changed files with 327 additions and 58 deletions
|
@ -194,6 +194,10 @@
|
||||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"block": "Block",
|
"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",
|
"time_table_header": "Time",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"domain_name_table_header": "Domain name",
|
"domain_name_table_header": "Domain name",
|
||||||
|
@ -569,5 +573,6 @@
|
||||||
"setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
|
"setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
|
||||||
"original_response": "Original response",
|
"original_response": "Original response",
|
||||||
"click_to_view_queries": "Click to view queries",
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -545,15 +545,17 @@ export const removeStaticLease = (config) => async (dispatch) => {
|
||||||
|
|
||||||
export const removeToast = createAction('REMOVE_TOAST');
|
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 { userRules } = getState().filtering;
|
||||||
|
|
||||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||||
const baseRule = `||${domain}^$important`;
|
|
||||||
const baseUnblocking = `@@${baseRule}`;
|
|
||||||
|
|
||||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
|
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;
|
||||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
|
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
|
||||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||||
|
|
||||||
|
@ -576,3 +578,10 @@ export const toggleBlocking = (type, domain) => async (dispatch, getState) => {
|
||||||
|
|
||||||
dispatch(getFilteringStatus());
|
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 {
|
.select--no-warning {
|
||||||
margin-bottom: 1.375rem;
|
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 type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
|
||||||
const text = type;
|
const text = type;
|
||||||
|
|
||||||
const className = classNames('btn btn-sm', {
|
const buttonClass = classNames('button-action button-action--main', {
|
||||||
'btn-outline-danger': isNotFound,
|
'button-action--unblock': !isNotFound,
|
||||||
'btn-outline-secondary': !isNotFound,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleClientStatus = (type, ip) => {
|
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));
|
dispatch(toggleClientBlock(type, ip));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -69,7 +70,7 @@ const renderBlockingButton = (ip) => {
|
||||||
return <div className="table__action pl-4">
|
return <div className="table__action pl-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={className}
|
className={buttonClass}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={processingSet}
|
disabled={processingSet}
|
||||||
>
|
>
|
||||||
|
@ -82,7 +83,7 @@ const ClientCell = (row) => {
|
||||||
const { value, original: { info } } = row;
|
const { value, original: { info } } = row;
|
||||||
|
|
||||||
return <>
|
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)}
|
{renderFormattedClientCell(value, info, true)}
|
||||||
{renderBlockingButton(value)}
|
{renderBlockingButton(value)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -164,6 +164,10 @@
|
||||||
color: #9aa0ac;
|
color: #9aa0ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-icon--white {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.header-brand-img {
|
.header-brand-img {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import propTypes from 'prop-types';
|
import propTypes from 'prop-types';
|
||||||
import { checkFiltered } from '../../../helpers/helpers';
|
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
|
||||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||||
import { toggleBlocking } from '../../../actions';
|
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||||
import IconTooltip from './IconTooltip';
|
import IconTooltip from './IconTooltip';
|
||||||
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||||
|
import { toggleClientBlock } from '../../../actions/access';
|
||||||
|
import { getBlockClientInfo } from './helpers';
|
||||||
|
|
||||||
const ClientCell = ({
|
const ClientCell = ({
|
||||||
client,
|
client,
|
||||||
|
@ -22,6 +24,12 @@ const ClientCell = ({
|
||||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
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 autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||||
const source = autoClient?.source;
|
const source = autoClient?.source;
|
||||||
|
@ -53,22 +61,81 @@ const ClientCell = ({
|
||||||
|
|
||||||
const renderBlockingButton = (isFiltered, domain) => {
|
const renderBlockingButton = (isFiltered, domain) => {
|
||||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
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', {
|
const {
|
||||||
'btn-outline-secondary': isFiltered,
|
confirmMessage,
|
||||||
'btn-outline-danger': !isFiltered,
|
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));
|
const onClick = () => dispatch(toggleBlocking(buttonType, domain));
|
||||||
|
|
||||||
return <button
|
const getOptions = (optionToActionMap) => {
|
||||||
type="button"
|
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}
|
className={buttonClass}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={processingRules}
|
disabled={processingRules}
|
||||||
>
|
>
|
||||||
{t(buttonType)}
|
{t(buttonType)}
|
||||||
</button>;
|
</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">
|
return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
|
||||||
|
@ -81,9 +148,7 @@ const ClientCell = ({
|
||||||
</div>
|
</div>
|
||||||
{isDetailed && name && !whoisAvailable
|
{isDetailed && name && !whoisAvailable
|
||||||
&& <div className="detailed-info d-none d-sm-block logs__text"
|
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||||
title={name}>
|
title={name}>{name}</div>}
|
||||||
{name}
|
|
||||||
</div>}
|
|
||||||
</div>
|
</div>
|
||||||
{renderBlockingButton(isFiltered, domain)}
|
{renderBlockingButton(isFiltered, domain)}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -6,17 +6,21 @@ import { processContent } from '../../../helpers/helpers';
|
||||||
import Tooltip from '../../ui/Tooltip';
|
import Tooltip from '../../ui/Tooltip';
|
||||||
import 'react-popper-tooltip/dist/styles.css';
|
import 'react-popper-tooltip/dist/styles.css';
|
||||||
import './IconTooltip.css';
|
import './IconTooltip.css';
|
||||||
|
import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
|
||||||
|
|
||||||
const IconTooltip = ({
|
const IconTooltip = ({
|
||||||
className,
|
className,
|
||||||
contentItemClass,
|
contentItemClass,
|
||||||
columnClass,
|
columnClass,
|
||||||
|
triggerClass,
|
||||||
canShowTooltip = true,
|
canShowTooltip = true,
|
||||||
xlinkHref,
|
xlinkHref,
|
||||||
title,
|
title,
|
||||||
placement,
|
placement,
|
||||||
tooltipClass,
|
tooltipClass,
|
||||||
content,
|
content,
|
||||||
|
trigger,
|
||||||
|
onVisibilityChange,
|
||||||
renderContent = content ? React.Children.map(
|
renderContent = content ? React.Children.map(
|
||||||
processContent(content),
|
processContent(content),
|
||||||
(item, idx) => <div key={idx} className={contentItemClass}>
|
(item, idx) => <div key={idx} className={contentItemClass}>
|
||||||
|
@ -36,6 +40,10 @@ const IconTooltip = ({
|
||||||
className={tooltipClassName}
|
className={tooltipClassName}
|
||||||
content={tooltipContent}
|
content={tooltipContent}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
triggerClass={triggerClass}
|
||||||
|
trigger={trigger}
|
||||||
|
onVisibilityChange={onVisibilityChange}
|
||||||
|
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
|
||||||
>
|
>
|
||||||
{xlinkHref && <svg className={className}>
|
{xlinkHref && <svg className={className}>
|
||||||
<use xlinkHref={`#${xlinkHref}`} />
|
<use xlinkHref={`#${xlinkHref}`} />
|
||||||
|
@ -45,6 +53,8 @@ const IconTooltip = ({
|
||||||
|
|
||||||
IconTooltip.propTypes = {
|
IconTooltip.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
trigger: PropTypes.string,
|
||||||
|
triggerClass: PropTypes.string,
|
||||||
contentItemClass: PropTypes.string,
|
contentItemClass: PropTypes.string,
|
||||||
columnClass: PropTypes.string,
|
columnClass: PropTypes.string,
|
||||||
tooltipClass: PropTypes.string,
|
tooltipClass: PropTypes.string,
|
||||||
|
@ -52,11 +62,9 @@ IconTooltip.propTypes = {
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
canShowTooltip: PropTypes.bool,
|
canShowTooltip: PropTypes.bool,
|
||||||
xlinkHref: PropTypes.string,
|
xlinkHref: PropTypes.string,
|
||||||
content: PropTypes.oneOfType([
|
content: PropTypes.node,
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||||
|
onVisibilityChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IconTooltip;
|
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,
|
formatDateTime,
|
||||||
formatElapsedMs,
|
formatElapsedMs,
|
||||||
formatTime,
|
formatTime,
|
||||||
|
getBlockingClientName,
|
||||||
getFilterName,
|
getFilterName,
|
||||||
processContent,
|
processContent,
|
||||||
} from '../../../helpers/helpers';
|
} from '../../../helpers/helpers';
|
||||||
|
@ -22,12 +23,14 @@ import {
|
||||||
SCHEME_TO_PROTOCOL_MAP,
|
SCHEME_TO_PROTOCOL_MAP,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||||
import { toggleBlocking } from '../../../actions';
|
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||||
import DateCell from './DateCell';
|
import DateCell from './DateCell';
|
||||||
import DomainCell from './DomainCell';
|
import DomainCell from './DomainCell';
|
||||||
import ResponseCell from './ResponseCell';
|
import ResponseCell from './ResponseCell';
|
||||||
import ClientCell from './ClientCell';
|
import ClientCell from './ClientCell';
|
||||||
import '../Logs.css';
|
import '../Logs.css';
|
||||||
|
import { toggleClientBlock } from '../../../actions/access';
|
||||||
|
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
|
||||||
|
|
||||||
const Row = memo(({
|
const Row = memo(({
|
||||||
style,
|
style,
|
||||||
|
@ -45,6 +48,13 @@ const Row = memo(({
|
||||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||||
const autoClients = useSelector((state) => state.dashboard.autoClients, 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 = () => {
|
const onClick = () => {
|
||||||
if (!isSmallScreen) { return; }
|
if (!isSmallScreen) { return; }
|
||||||
const {
|
const {
|
||||||
|
@ -98,6 +108,26 @@ const Row = memo(({
|
||||||
|
|
||||||
const filter = getFilterName(filters, whitelistFilters, filterId);
|
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 = {
|
const detailedData = {
|
||||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||||
|
@ -132,10 +162,12 @@ const Row = memo(({
|
||||||
source_label: source,
|
source_label: source,
|
||||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||||
original_response: originalResponse?.join('\n'),
|
original_response: originalResponse?.join('\n'),
|
||||||
[buttonType]: <div onClick={onToggleBlock}
|
[BUTTON_PREFIX + buttonType]: <div onClick={onToggleBlock}
|
||||||
className={classNames('title--border text-center', {
|
className={classNames('title--border text-center', {
|
||||||
'bg--danger': isBlocked,
|
'bg--danger': isBlocked,
|
||||||
})}>{t(buttonType)}</div>,
|
})}>{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));
|
setDetailedDataCurrent(processContent(detailedData));
|
||||||
|
|
|
@ -10,9 +10,20 @@
|
||||||
--size-client: 123;
|
--size-client: 123;
|
||||||
--gray-216: rgba(216, 216, 216, 0.23);
|
--gray-216: rgba(216, 216, 216, 0.23);
|
||||||
--gray-4d: #4D4D4D;
|
--gray-4d: #4D4D4D;
|
||||||
|
--gray-f3: #F3F3F3;
|
||||||
--gray-8: #888;
|
--gray-8: #888;
|
||||||
--danger: #DF3812;
|
--danger: #DF3812;
|
||||||
--white80: rgba(255, 255, 255, 0.8);
|
--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 {
|
.logs__text {
|
||||||
|
@ -191,6 +202,7 @@
|
||||||
width: 7.6875rem;
|
width: 7.6875rem;
|
||||||
flex: var(--size-client) 0 auto;
|
flex: var(--size-client) 0 auto;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__cell--header__container > .logs__cell--header__item {
|
.logs__cell--header__container > .logs__cell--header__item {
|
||||||
|
@ -202,12 +214,95 @@
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__cell--block-button {
|
.button-action__container {
|
||||||
max-height: 1.75rem;
|
display: flex;
|
||||||
position: relative;
|
position: absolute;
|
||||||
left: 10%;
|
right: 0;
|
||||||
top: 40%;
|
bottom: 0.5rem;
|
||||||
visibility: hidden;
|
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 {
|
.logs__row {
|
||||||
|
@ -222,14 +317,6 @@
|
||||||
border-bottom: 2px solid var(--gray-216);
|
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 */
|
/* QUERY_STATUS_COLORS */
|
||||||
.logs__row--blue {
|
.logs__row--blue {
|
||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
|
|
|
@ -23,15 +23,16 @@ import {
|
||||||
} from '../../actions/queryLogs';
|
} from '../../actions/queryLogs';
|
||||||
import InfiniteTable from './InfiniteTable';
|
import InfiniteTable from './InfiniteTable';
|
||||||
import './Logs.css';
|
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]) => {
|
.map(([key, value]) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTitle = value === 'title';
|
const isTitle = value === 'title';
|
||||||
const isButton = key === buttonType;
|
const isButton = key.startsWith(BUTTON_PREFIX);
|
||||||
const isBoolean = typeof value === 'boolean';
|
const isBoolean = typeof value === 'boolean';
|
||||||
const isHidden = isBoolean && value === false;
|
const isHidden = isBoolean && value === false;
|
||||||
|
|
||||||
|
|
|
@ -344,6 +344,14 @@ const Icons = () => (
|
||||||
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
||||||
</svg>
|
</svg>
|
||||||
</symbol>
|
</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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ const Tooltip = ({
|
||||||
trigger = 'hover',
|
trigger = 'hover',
|
||||||
delayShow = SHOW_TOOLTIP_DELAY,
|
delayShow = SHOW_TOOLTIP_DELAY,
|
||||||
delayHide = HIDE_TOOLTIP_DELAY,
|
delayHide = HIDE_TOOLTIP_DELAY,
|
||||||
|
onVisibilityChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const touchEventsAvailable = 'ontouchstart' in window;
|
const touchEventsAvailable = 'ontouchstart' in window;
|
||||||
|
@ -73,6 +74,7 @@ const Tooltip = ({
|
||||||
delayHide={delayHideValue}
|
delayHide={delayHideValue}
|
||||||
delayShow={delayShowValue}
|
delayShow={delayShowValue}
|
||||||
tooltip={renderTooltip}
|
tooltip={renderTooltip}
|
||||||
|
onVisibilityChange={onVisibilityChange}
|
||||||
>
|
>
|
||||||
{renderTrigger}
|
{renderTrigger}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
@ -90,10 +92,11 @@ Tooltip.propTypes = {
|
||||||
).isRequired,
|
).isRequired,
|
||||||
placement: propTypes.string,
|
placement: propTypes.string,
|
||||||
trigger: propTypes.string,
|
trigger: propTypes.string,
|
||||||
delayHide: propTypes.string,
|
delayHide: propTypes.number,
|
||||||
delayShow: propTypes.string,
|
delayShow: propTypes.number,
|
||||||
className: propTypes.string,
|
className: propTypes.string,
|
||||||
triggerClass: propTypes.string,
|
triggerClass: propTypes.string,
|
||||||
|
onVisibilityChange: propTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip;
|
||||||
|
|
|
@ -836,3 +836,21 @@ export const isScrolledIntoView = (el) => {
|
||||||
|
|
||||||
return elemTop < window.innerHeight && elemBottom >= 0;
|
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;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue