Pull request: 684 move block/unblock button to the tooltip menu

Updates #684

Squashed commit of the following:

commit 2c5ac44d3edb55d0d3be169fb0bbfb336920964d
Merge: 0fce61dfa 733d6c1fc
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Oct 16 10:08:58 2023 +0300

    Merge branch 'master' into ADG-7200

commit 0fce61dfad31951c3d5a484a14383c58964b47f0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:38:13 2023 +0300

    fix dashboard clients table block/unblock button

commit ef5d72fd7cac81b70503c8813cf6e96952ec3b65
Merge: f49281b88 506d71310
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:16:21 2023 +0300

    Merge branch 'master' into ADG-7200

commit f49281b8818728ab298d769b8164919ea415d6c9
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:25:05 2023 +0300

    fix disabled

commit 0031861174108214e9741852f6e00c5873c3b20f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:20:50 2023 +0300

    fix icon color

commit 2916937b0d6a0d126bd48e59f41a6fcbc4622ea1
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:15:55 2023 +0300

    changelog

commit 34493c974fb9304267e9149360c802d9364dd7de
Merge: d3267bbe9 d3fabdda4
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:13:43 2023 +0300

    Merge branch 'master' into ADG-7200

commit d3267bbe9477d6db0783534cb4ab4f7afc4d63b0
Merge: 58a6c766f 6a3661562
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Oct 11 16:46:39 2023 +0300

    Merge branch 'master' into ADG-7200

commit 58a6c766f3b783f0e2fdc36d40889042f3c74f2f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Oct 11 16:46:13 2023 +0300

    ADG-7200 move block/unblock button to the tooltip menu
This commit is contained in:
Ildar Kamalov 2023-10-16 18:00:28 +03:00
parent 733d6c1fca
commit 413f484810
11 changed files with 124 additions and 102 deletions

View file

@ -23,12 +23,17 @@ See also the [v0.107.40 GitHub milestone][ms-v0.107.40].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Changed
- "Block" and "Unblock" buttons of the query log moved to the tooltip menu ([#684]).
### Fixed ### Fixed
- The time shown in the statistics is one hour less than the current time ([#6296]). - The time shown in the statistics is one hour less than the current time ([#6296]).
- Issues with QUIC and HTTP/3 upstreams on FreeBSD ([#6301]). - Issues with QUIC and HTTP/3 upstreams on FreeBSD ([#6301]).
- Panic on clearing query log ([#6304]). - Panic on clearing query log ([#6304]).
[#684]: https://github.com/AdguardTeam/AdGuardHome/issues/684
[#6296]: https://github.com/AdguardTeam/AdGuardHome/issues/6296 [#6296]: https://github.com/AdguardTeam/AdGuardHome/issues/6296
[#6301]: https://github.com/AdguardTeam/AdGuardHome/issues/6301 [#6301]: https://github.com/AdguardTeam/AdGuardHome/issues/6301
[#6304]: https://github.com/AdguardTeam/AdGuardHome/issues/6304 [#6304]: https://github.com/AdguardTeam/AdGuardHome/issues/6304

View file

@ -134,15 +134,6 @@ body {
cursor: not-allowed; cursor: not-allowed;
} }
.button-action {
visibility: hidden;
}
.logs__row:hover .button-action,
.button-action--active {
visibility: visible;
}
.ReactModal__Body--open { .ReactModal__Body--open {
overflow: hidden; overflow: hidden;
} }

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
@ -13,6 +13,7 @@ import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access'; import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell'; import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats'; import { getStats } from '../../actions/stats';
import IconTooltip from '../Logs/Cells/IconTooltip';
const getClientsPercentColor = (percent) => { const getClientsPercentColor = (percent) => {
if (percent > 50) { if (percent > 50) {
@ -40,9 +41,7 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const processingSet = useSelector((state) => state.access.processingSet); const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual); const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const buttonClass = classNames('button-action button-action--main', { const [isOptionsOpened, setOptionsOpened] = useState(false);
'button-action--unblock': disallowed,
});
const toggleClientStatus = async (ip, disallowed, disallowed_rule) => { const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
let confirmMessage; let confirmMessage;
@ -62,23 +61,49 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
} }
}; };
const onClick = () => toggleClientStatus(ip, disallowed, disallowed_rule); const onClick = () => {
toggleClientStatus(ip, disallowed, disallowed_rule);
setOptionsOpened(false);
};
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK; const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule; const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
const disabled = processingSet || lastRuleInAllowlist; const disabled = processingSet || lastRuleInAllowlist;
return ( return (
<div className="table__action pl-4"> <div className="table__action">
<button <button
type="button" type="button"
className={buttonClass} className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
<svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" />
</svg>
</button>
{isOptionsOpened && (
<IconTooltip
className="icon24"
tooltipClass="button-action--arrow-option-container"
xlinkHref="bullets"
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={(
<button
className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''} title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
> >
<Trans>{text}</Trans> <Trans>{text}</Trans>
</button> </button>
)}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
defaultTooltipShown={true}
delayHide={0}
/>
)}
</div> </div>
); );
}; };

View file

@ -28,11 +28,13 @@
border-bottom: 6px solid #585965; border-bottom: 6px solid #585965;
} }
@media (max-width: 1279.98px) { .table__action {
.table__action { position: relative;
position: absolute; margin-left: auto;
right: 0; }
}
.table__action .btn-icon {
margin: 2px;
} }
.page-title--dashboard { .page-title--dashboard {

View file

@ -26,9 +26,7 @@ const ClientCell = ({
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual); const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const processingRules = useSelector((state) => state.filtering.processingRules);
const isDetailed = useSelector((state) => state.queryLogs.isDetailed); const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual); const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const [isOptionsOpened, setOptionsOpened] = useState(false); const [isOptionsOpened, setOptionsOpened] = useState(false);
@ -84,11 +82,23 @@ const ClientCell = ({
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client); const clientNameBlockingFor = getBlockingClientName(clients, client);
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
setOptionsOpened(false);
};
const BUTTON_OPTIONS = [ const BUTTON_OPTIONS = [
{
name: buttonType,
onClick,
className: isFiltered ? 'bg--green' : 'bg--danger',
},
{ {
name: blockingForClientKey, name: blockingForClientKey,
onClick: () => { onClick: () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor)); dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
setOptionsOpened(false);
}, },
}, },
{ {
@ -101,27 +111,25 @@ const ClientCell = ({
client_info?.disallowed_rule || '', client_info?.disallowed_rule || '',
)); ));
await dispatch(updateLogs()); await dispatch(updateLogs());
setOptionsOpened(false);
} }
}, },
disabled: processingSet || lastRuleInAllowlist, disabled: lastRuleInAllowlist,
}, },
]; ];
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
};
const getOptions = (options) => { const getOptions = (options) => {
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }
return ( return (
<> <>
{options.map(({ name, onClick, disabled }) => ( {options.map(({
name, onClick, disabled, className,
}) => (
<button <button
key={name} key={name}
className="button-action--arrow-option px-4 py-1" className={classNames('button-action--arrow-option px-4 py-1', className)}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
> >
@ -134,17 +142,6 @@ const ClientCell = ({
const content = getOptions(BUTTON_OPTIONS); const content = getOptions(BUTTON_OPTIONS);
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', { const containerClass = classNames('button-action__container', {
'button-action__container--detailed': isDetailed, 'button-action__container--detailed': isDetailed,
}); });
@ -153,25 +150,26 @@ const ClientCell = ({
<div className={containerClass}> <div className={containerClass}>
<button <button
type="button" type="button"
className={buttonClass} className="btn btn-icon btn-sm px-0"
onClick={onClick} onClick={() => setOptionsOpened(true)}
disabled={processingRules}
> >
{t(buttonType)} <svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" />
</svg>
</button> </button>
{content && ( {isOptionsOpened && (
<button className={buttonArrowClass} disabled={processingRules}>
<IconTooltip <IconTooltip
className="icon24" className="icon24"
tooltipClass="button-action--arrow-option-container" tooltipClass="button-action--arrow-option-container"
xlinkHref="chevron-down" xlinkHref="bullets"
triggerClass="button-action--icon" triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={content} content={content}
placement="bottom-end" placement="bottom-end"
trigger="click" trigger="click"
onVisibilityChange={setOptionsOpened} onVisibilityChange={setOptionsOpened}
defaultTooltipShown={true}
delayHide={0}
/> />
</button>
)} )}
</div> </div>
); );
@ -198,7 +196,7 @@ const ClientCell = ({
</div> </div>
{isDetailed && clientName && !whoisAvailable && ( {isDetailed && clientName && !whoisAvailable && (
<Link <Link
className="detailed-info d-none d-sm-block logs__text logs__text--link" className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
to={`logs?search="${encodeURIComponent(clientName)}"`} to={`logs?search="${encodeURIComponent(clientName)}"`}
title={clientName} title={clientName}
> >

View file

@ -1,4 +1,5 @@
.tooltip-custom__container { .tooltip-custom__container {
min-width: 150px;
padding: 1rem 1.5rem 1.25rem 1.5rem; padding: 1rem 1.5rem 1.25rem 1.5rem;
font-size: 16px !important; font-size: 16px !important;
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);

View file

@ -21,6 +21,8 @@ const IconTooltip = ({
content, content,
trigger, trigger,
onVisibilityChange, onVisibilityChange,
defaultTooltipShown,
delayHide,
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}>
@ -44,6 +46,8 @@ const IconTooltip = ({
trigger={trigger} trigger={trigger}
onVisibilityChange={onVisibilityChange} onVisibilityChange={onVisibilityChange}
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY} delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
delayHide={delayHide}
defaultTooltipShown={defaultTooltipShown}
> >
{xlinkHref && <svg className={className}> {xlinkHref && <svg className={className}>
<use xlinkHref={`#${xlinkHref}`} /> <use xlinkHref={`#${xlinkHref}`} />
@ -65,6 +69,8 @@ IconTooltip.propTypes = {
content: PropTypes.node, content: PropTypes.node,
renderContent: PropTypes.arrayOf(PropTypes.element), renderContent: PropTypes.arrayOf(PropTypes.element),
onVisibilityChange: PropTypes.func, onVisibilityChange: PropTypes.func,
defaultTooltipShown: PropTypes.bool,
delayHide: PropTypes.number,
}; };
export default IconTooltip; export default IconTooltip;

View file

@ -80,6 +80,10 @@
color: var(--gray-f3); color: var(--gray-f3);
} }
.logs__text--client {
padding-right: 32px;
}
.icon--selected { .icon--selected {
background-color: var(--gray-f3); background-color: var(--gray-f3);
border: solid 1px var(--gray-d8); border: solid 1px var(--gray-d8);
@ -261,9 +265,8 @@
.button-action__container { .button-action__container {
display: flex; display: flex;
position: absolute; position: absolute;
right: 0; right: 2px;
bottom: 0.5rem; bottom: 0.5rem;
height: 1.6rem;
} }
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
@ -307,45 +310,10 @@
border-bottom-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 { .button-action:hover {
cursor: pointer; cursor: pointer;
} }
.button-action--arrow .button-action--icon {
width: 100%;
height: 100%;
display: flex;
justify-content: 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 { .button-action--arrow-option {
background: transparent; background: transparent;
border: 0; border: 0;
@ -551,3 +519,20 @@
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
background-color: var(--card-bgcolor); background-color: var(--card-bgcolor);
} }
.button-action__hidden-trigger {
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 33px;
margin: -1px;
padding: 0;
overflow: hidden;
border: 0;
clip: rect(0 0 0 0);
}
[data-theme="dark"] .button-action__icon {
color: var(--gray-f3);
}

View file

@ -239,6 +239,12 @@ const Icons = () => (
<circle cx="12" cy="12" r="9" /> <circle cx="12" cy="12" r="9" />
<path d="M16.1215 12.1213H11.8789V7.87866" /> <path d="M16.1215 12.1213H11.8789V7.87866" />
</symbol> </symbol>
<symbol id="bullets" width="24" height="24" viewBox="0 0 24 24">
<path fillRule="evenodd" clipRule="evenodd" d="M12 7C11.1716 7 10.5 6.32843 10.5 5.5C10.5 4.67157 11.1716 4 12 4C12.8284 4 13.5 4.67157 13.5 5.5C13.5 6.32843 12.8284 7 12 7Z" fill="currentColor" />
<path fillRule="evenodd" clipRule="evenodd" d="M12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5Z" fill="currentColor" />
<path fillRule="evenodd" clipRule="evenodd" d="M12 20C11.1716 20 10.5 19.3284 10.5 18.5C10.5 17.6716 11.1716 17 12 17C12.8284 17 13.5 17.6716 13.5 18.5C13.5 19.3284 12.8284 20 12 20Z" fill="currentColor" />
</symbol>
</svg> </svg>
); );

View file

@ -21,6 +21,7 @@ const Tooltip = ({
delayShow = SHOW_TOOLTIP_DELAY, delayShow = SHOW_TOOLTIP_DELAY,
delayHide = HIDE_TOOLTIP_DELAY, delayHide = HIDE_TOOLTIP_DELAY,
onVisibilityChange, onVisibilityChange,
defaultTooltipShown,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const touchEventsAvailable = 'ontouchstart' in window; const touchEventsAvailable = 'ontouchstart' in window;
@ -75,6 +76,7 @@ const Tooltip = ({
delayShow={delayShowValue} delayShow={delayShowValue}
tooltip={renderTooltip} tooltip={renderTooltip}
onVisibilityChange={onVisibilityChange} onVisibilityChange={onVisibilityChange}
defaultTooltipShown={defaultTooltipShown}
> >
{renderTrigger} {renderTrigger}
</TooltipTrigger> </TooltipTrigger>
@ -97,6 +99,7 @@ Tooltip.propTypes = {
className: propTypes.string, className: propTypes.string,
triggerClass: propTypes.string, triggerClass: propTypes.string,
onVisibilityChange: propTypes.func, onVisibilityChange: propTypes.func,
defaultTooltipShown: propTypes.bool,
}; };
export default Tooltip; export default Tooltip;

View file

@ -43,7 +43,7 @@ export const renderFormattedClientCell = (value, info, isDetailed = false, isLog
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0; const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
if (name) { if (name) {
const nameValue = <div className="logs__text logs__text--link logs__text--nowrap" title={`${name} (${value})`}> const nameValue = <div className="logs__text logs__text--link logs__text--nowrap logs__text--client" title={`${name} (${value})`}>
{name}&nbsp;<small>{`(${value})`}</small> {name}&nbsp;<small>{`(${value})`}</small>
</div>; </div>;