From 8dc010886867de62340d933df7edc31314234abc Mon Sep 17 00:00:00 2001 From: Artem Baskal Date: Sat, 5 Sep 2020 10:22:47 +0300 Subject: [PATCH] + client: Redesign query logs block/unblock buttons Close #2050 Squashed commit of the following: commit 3bc6a409034989b914306e1c33da274730ca623e Author: ArtemBaskal Date: Fri Sep 4 20:58:09 2020 +0300 Change dashboard block confirm message commit d4d47c3557e2166ee04db25a71b782bfbfe3b865 Merge: e8865827 fc43e2ac Author: ArtemBaskal Date: Fri Sep 4 14:56:34 2020 +0300 Merge branch 'master' into feature/2050 commit e8865827879955b1ef62c9ff85798d07bfa4627d Author: ArtemBaskal Date: Fri Sep 4 13:46:10 2020 +0300 Rename classname commit 648151c54e493c63622e014cb9cd1cb450f25478 Author: ArtemBaskal Date: Thu Sep 3 19:09:21 2020 +0300 Decrease arrow size commit 4feadab707c613d31225dfa9443a9a836db37ba1 Author: ArtemBaskal Date: Thu Sep 3 18:27:41 2020 +0300 Rename button class commit c3919d8ae8d1431657ce61afad2c20e5806f279a Author: ArtemBaskal Date: Thu Sep 3 10:35:15 2020 +0300 Review changes: extract variables commit 0ac809584c391e41a1749a844bc1075e05a92345 Author: ArtemBaskal Date: Thu Sep 3 10:13:57 2020 +0300 Display dashboard button on hover commit 1395287c2383e2248a2a5d39451403bd73141e55 Author: ArtemBaskal Date: Wed Sep 2 21:24:04 2020 +0300 Do not hide button on option open commit 947f254b7aea26f289b66b66fac46dba11ea3952 Author: ArtemBaskal Date: Wed Sep 2 21:20:19 2020 +0300 Add buttons for mobile screen commit df05697f87163a2b716d82653884e631f2fa6cf3 Author: ArtemBaskal Date: Wed Sep 2 20:18:20 2020 +0300 Change dashboard button styles commit 16655f2d6b0d79d1fa027ec2310bb0268fffaf6a Author: ArtemBaskal Date: Wed Sep 2 20:04:28 2020 +0300 Change button styles, rename button options commit 1ac22e875d8b26c16830bf6edb85dadcc19ff287 Author: ArtemBaskal Date: Wed Sep 2 19:30:16 2020 +0300 Review changes commit c590119875439d85927bdd334658e003bc1f0563 Author: ArtemBaskal Date: Wed Sep 2 17:58:08 2020 +0300 Remove default query logs form values commit 141329563417f5337f5659d5500f4cbe16d64bd2 Author: ArtemBaskal Date: Wed Sep 2 17:41:23 2020 +0300 Update blocking buttons options logic, fix button svg size commit 9e4f39aa6cb8e134d80d496b8a248b2fe6aceb99 Author: ArtemBaskal Date: Wed Sep 2 16:30:48 2020 +0300 Fix button position commit 8aabff7daccb87ae02c2302e62e296b3cfc17608 Merge: 415a0334 6b614295 Author: ArtemBaskal Date: Tue Sep 1 17:29:55 2020 +0300 Merge branch 'master' into feature/2050 commit 415a0334561733d92a0f7badd68101ef554dc689 Author: ArtemBaskal Date: Tue Sep 1 17:05:51 2020 +0300 Add blocking options commit bc6aed92b6e12f27c2604501275b53bb8159d5bc Merge: 0de4fb3a 40b74522 Author: ArtemBaskal Date: Tue Sep 1 15:49:06 2020 +0300 Merge branch 'feature/infinite_scroll_query_logs' into feature/2050 commit 40b745225112cf8d664220ed8f484b0aa16e997c Author: ArtemBaskal Date: Tue Sep 1 15:46:27 2020 +0300 Remove dynamic translation of toasts commit 0de4fb3a4cd785c6b52e860e204c6e13d356b178 Merge: 1ab14471 f08fa7b8 Author: ArtemBaskal Date: Tue Sep 1 15:07:30 2020 +0300 Merge branch 'feature/infinite_scroll_query_logs' into feature/2050 ... and 51 more commits --- client/src/__locales/en.json | 7 +- client/src/actions/index.js | 19 ++- client/src/components/App/index.css | 9 ++ client/src/components/Dashboard/Clients.js | 15 +-- client/src/components/Header/Header.css | 4 + .../src/components/Logs/Cells/ClientCell.js | 101 ++++++++++++--- .../src/components/Logs/Cells/IconTooltip.js | 16 ++- .../components/Logs/Cells/helpers/index.js | 19 +++ client/src/components/Logs/Cells/index.js | 42 ++++++- client/src/components/Logs/Logs.css | 115 +++++++++++++++--- client/src/components/Logs/index.js | 5 +- client/src/components/ui/Icons.js | 8 ++ client/src/components/ui/Tooltip.js | 7 +- client/src/helpers/helpers.js | 18 +++ 14 files changed, 327 insertions(+), 58 deletions(-) create mode 100644 client/src/components/Logs/Cells/helpers/index.js 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 instruction on how to resolve this." + "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this.", + "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client." } 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
; + const getOptions = (optionToActionMap) => { + const options = Object.entries(optionToActionMap); + if (options.length === 0) { + return null; + } + return <>{options + .map(([name, onClick]) =>
{t(name)} +
)}; + }; + + const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP); + + const buttonClass = classNames('button-action button-action--main', { + 'button-action--unblock': isFiltered, + 'button-action--with-options': content, + 'button-action--active': isOptionsOpened, + }); + + const buttonArrowClass = classNames('button-action button-action--arrow', { + 'button-action--unblock': isFiltered, + 'button-action--active': isOptionsOpened, + }); + + const containerClass = classNames('button-action__container', { + 'button-action__container--detailed': isDetailed, + }); + + return
+ + {content && } +
; }; return
@@ -81,9 +148,7 @@ const ClientCell = ({
{isDetailed && name && !whoisAvailable &&
- {name} -
} + title={name}>{name}
} {renderBlockingButton(isFiltered, domain)} ; diff --git a/client/src/components/Logs/Cells/IconTooltip.js b/client/src/components/Logs/Cells/IconTooltip.js index 5b9cc2cb..8bb3d624 100644 --- a/client/src/components/Logs/Cells/IconTooltip.js +++ b/client/src/components/Logs/Cells/IconTooltip.js @@ -6,17 +6,21 @@ import { processContent } from '../../../helpers/helpers'; import Tooltip from '../../ui/Tooltip'; import 'react-popper-tooltip/dist/styles.css'; import './IconTooltip.css'; +import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants'; const IconTooltip = ({ className, contentItemClass, columnClass, + triggerClass, canShowTooltip = true, xlinkHref, title, placement, tooltipClass, content, + trigger, + onVisibilityChange, renderContent = content ? React.Children.map( processContent(content), (item, idx) =>
@@ -36,6 +40,10 @@ const IconTooltip = ({ className={tooltipClassName} content={tooltipContent} placement={placement} + triggerClass={triggerClass} + trigger={trigger} + onVisibilityChange={onVisibilityChange} + delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY} > {xlinkHref && @@ -45,6 +53,8 @@ const IconTooltip = ({ IconTooltip.propTypes = { className: PropTypes.string, + trigger: PropTypes.string, + triggerClass: PropTypes.string, contentItemClass: PropTypes.string, columnClass: PropTypes.string, tooltipClass: PropTypes.string, @@ -52,11 +62,9 @@ IconTooltip.propTypes = { placement: PropTypes.string, canShowTooltip: PropTypes.bool, xlinkHref: PropTypes.string, - content: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array, - ]), + content: PropTypes.node, renderContent: PropTypes.arrayOf(PropTypes.element), + onVisibilityChange: PropTypes.func, }; export default IconTooltip; diff --git a/client/src/components/Logs/Cells/helpers/index.js b/client/src/components/Logs/Cells/helpers/index.js new file mode 100644 index 00000000..61e7ff5c --- /dev/null +++ b/client/src/components/Logs/Cells/helpers/index.js @@ -0,0 +1,19 @@ +import { getIpMatchListStatus } from '../../../../helpers/helpers'; +import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants'; + +export const BUTTON_PREFIX = 'btn_'; + +export const getBlockClientInfo = (client, disallowed_clients) => { + const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients); + + const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND; + const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK; + + const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock'; + const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client'; + return { + confirmMessage, + buttonKey, + type, + }; +}; diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js index c2d7968b..8a0fced3 100644 --- a/client/src/components/Logs/Cells/index.js +++ b/client/src/components/Logs/Cells/index.js @@ -9,6 +9,7 @@ import { formatDateTime, formatElapsedMs, formatTime, + getBlockingClientName, getFilterName, processContent, } from '../../../helpers/helpers'; @@ -22,12 +23,14 @@ import { SCHEME_TO_PROTOCOL_MAP, } from '../../../helpers/constants'; import { getSourceData } from '../../../helpers/trackers/trackers'; -import { toggleBlocking } from '../../../actions'; +import { toggleBlocking, toggleBlockingForClient } from '../../../actions'; import DateCell from './DateCell'; import DomainCell from './DomainCell'; import ResponseCell from './ResponseCell'; import ClientCell from './ClientCell'; import '../Logs.css'; +import { toggleClientBlock } from '../../../actions/access'; +import { getBlockClientInfo, BUTTON_PREFIX } from './helpers'; const Row = memo(({ style, @@ -45,6 +48,13 @@ const Row = memo(({ const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual); + const disallowed_clients = useSelector( + (state) => state.access.disallowed_clients, + shallowEqual, + ); + + const clients = useSelector((state) => state.dashboard.clients); + const onClick = () => { if (!isSmallScreen) { return; } const { @@ -98,6 +108,26 @@ const Row = memo(({ const filter = getFilterName(filters, whitelistFilters, filterId); + const { + confirmMessage, + buttonKey: blockingClientKey, + type: blockType, + } = getBlockClientInfo(client, disallowed_clients); + + const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; + const clientNameBlockingFor = getBlockingClientName(clients, client); + + const onBlockingForClientClick = () => { + dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor)); + }; + + const onBlockingClientClick = () => { + const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`; + if (window.confirm(message)) { + dispatch(toggleClientBlock(blockType, client)); + } + }; + const detailedData = { time_table_header: formatTime(time, LONG_TIME_FORMAT), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), @@ -132,10 +162,12 @@ const Row = memo(({ source_label: source, validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false, original_response: originalResponse?.join('\n'), - [buttonType]:
{t(buttonType)}
, + [BUTTON_PREFIX + buttonType]:
{t(buttonType)}
, + [BUTTON_PREFIX + blockingForClientKey]:
{t(blockingForClientKey)}
, + [BUTTON_PREFIX + blockingClientKey]:
{t(blockingClientKey)}
, }; setDetailedDataCurrent(processContent(detailedData)); diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 857fd466..fd088b79 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -10,9 +10,20 @@ --size-client: 123; --gray-216: rgba(216, 216, 216, 0.23); --gray-4d: #4D4D4D; + --gray-f3: #F3F3F3; --gray-8: #888; --danger: #DF3812; --white80: rgba(255, 255, 255, 0.8); + + --btn-block: #C23814; + --btn-block-disabled: #E3B3A6; + --btn-block-active: #A62200; + + --btn-unblock: #888888; + --btn-unblock-disabled: #D8D8D8; + --btn-unblock-active: #4D4D4D; + + --option-border-radius: 4px; } .logs__text { @@ -191,6 +202,7 @@ width: 7.6875rem; flex: var(--size-client) 0 auto; padding-right: 0; + position: relative; } .logs__cell--header__container > .logs__cell--header__item { @@ -202,12 +214,95 @@ padding-right: 0; } -.logs__cell--block-button { - max-height: 1.75rem; - position: relative; - left: 10%; - top: 40%; - visibility: hidden; +.button-action__container { + display: flex; + position: absolute; + right: 0; + bottom: 0.5rem; + height: 1.6rem; +} + +.button-action__container--detailed { + bottom: 1.3rem; +} + +.button-action { + outline: 0 !important; + background: var(--btn-block); + border-radius: var(--option-border-radius); + font-size: 0.8rem; + color: var(--white); + letter-spacing: 0; + text-align: center; + line-height: 28px; + border: 0; +} + +.button-action--unblock { + background: var(--btn-unblock); +} + +.button-action--main { + padding: 0 1rem; + display: flex; + align-items: center; +} + +.button-action--with-options { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.button-action--arrow { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid var(--white); + width: 1.5625rem; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.button-action:hover { + cursor: pointer; +} + +.button-action--arrow .button-action--icon { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.button-action:active { + background: var(--btn-block-active); +} + +.button-action--unblock:active { + background: var(--btn-unblock-active); +} + +.button-action:disabled { + background: var(--btn-block-disabled); + cursor: default; +} + +.button-action--unblock:disabled { + background: var(--btn-unblock-disabled); +} + +.button-action--arrow-option:hover { + cursor: pointer; + background: var(--gray-f3); + overflow: hidden; +} + +.button-action--arrow-option-container { + overflow: visible; + transform-origin: left; + padding: 1rem 0; } .logs__row { @@ -222,14 +317,6 @@ border-bottom: 2px solid var(--gray-216); } -.logs__table .logs__row:hover .logs__cell--block-button { - visibility: visible; -} - -.logs__table .logs__row .logs__cell--block-button:disabled { - background-color: var(--white) !important; -} - /* QUERY_STATUS_COLORS */ .logs__row--blue { background-color: var(--blue); diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 13fa697c..bcc9a94d 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -23,15 +23,16 @@ import { } from '../../actions/queryLogs'; import InfiniteTable from './InfiniteTable'; import './Logs.css'; +import { BUTTON_PREFIX } from './Cells/helpers'; -const processContent = (data, buttonType) => Object.entries(data) +const processContent = (data) => Object.entries(data) .map(([key, value]) => { if (!value) { return null; } const isTitle = value === 'title'; - const isButton = key === buttonType; + const isButton = key.startsWith(BUTTON_PREFIX); const isBoolean = typeof value === 'boolean'; const isHidden = isBoolean && value === false; diff --git a/client/src/components/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/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.} + * @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; +};