From 8dc010886867de62340d933df7edc31314234abc Mon Sep 17 00:00:00 2001 From: Artem Baskal <a.baskal@adguard.com> 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 <a.baskal@adguard.com> Date: Fri Sep 4 20:58:09 2020 +0300 Change dashboard block confirm message commit d4d47c3557e2166ee04db25a71b782bfbfe3b865 Merge: e8865827 fc43e2ac 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: 415a0334 6b614295 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 --- 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 | Bin 42118 -> 42479 bytes client/src/components/ui/Tooltip.js | 7 +- client/src/helpers/helpers.js | 18 +++ 14 files changed, 319 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</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." } 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 <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> diff --git a/client/src/components/Header/Header.css b/client/src/components/Header/Header.css index b67840f3..a5fe802e 100644 --- a/client/src/components/Header/Header.css +++ b/client/src/components/Header/Header.css @@ -164,6 +164,10 @@ color: #9aa0ac; } + .nav-icon--white { + color: #fff; + } + .header-brand-img { height: 32px; } diff --git a/client/src/components/Logs/Cells/ClientCell.js b/client/src/components/Logs/Cells/ClientCell.js index 93830faa..fd9875bf 100644 --- a/client/src/components/Logs/Cells/ClientCell.js +++ b/client/src/components/Logs/Cells/ClientCell.js @@ -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>; 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) => <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; 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]: <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)); 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 f29bcc21aaf23ebc611b2329d1785e21dd900b3b..3851ff014c13df989d2c8e8f44bd8a64b4946674 100644 GIT binary patch delta 169 zcmZoW$@Km-(}u()lMU2Gd6F|y%Zl>zbW`%n^CmA;78S7LQa}PW>60V6j3ysg$f;|f zV31*CQf6f0VNj){kd~Q~W2>ZWVGe>2u75#da%M@Tt&*OB;p7WlQj;f66`R};$v$~) z7tiFwwrZ0LdRZrbh-8!3GcYz&Ff`D!G|n*w5(*}I2Ie5b%t9B$pWNQ7F*$LGDgYIv BH2eSn delta 13 VcmaEVnyKw1(}u()lTR&G1pqRN2I>F+ 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} </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; 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.<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; +};