Merge branch 'master' into 1920-client-find

This commit is contained in:
ArtemBaskal 2020-09-07 16:13:42 +03:00
commit 458d9f49cb
17 changed files with 342 additions and 63 deletions

View File

@ -45,7 +45,7 @@
"dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!", "dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!",
"dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.", "dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.",
"dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.", "dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.",
"dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.", "dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.",
"dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt", "dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt",
"dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt", "dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt",
"dhcp_new_static_lease": "Neuer statischer Lease", "dhcp_new_static_lease": "Neuer statischer Lease",
@ -99,7 +99,7 @@
"no_clients_found": "Keine Clients gefunden", "no_clients_found": "Keine Clients gefunden",
"general_statistics": "Allgemeine Statistiken", "general_statistics": "Allgemeine Statistiken",
"number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen", "number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
"number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}}} Tagen verarbeitet wurden", "number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden",
"number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen", "number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
"number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen", "number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen",
"number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen", "number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen",

View File

@ -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."
} }

View File

@ -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);
};

View File

@ -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;
}

View File

@ -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>

View File

@ -164,6 +164,10 @@
color: #9aa0ac; color: #9aa0ac;
} }
.nav-icon--white {
color: #fff;
}
.header-brand-img { .header-brand-img {
height: 32px; height: 32px;
} }

View File

@ -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);
className={buttonClass} if (options.length === 0) {
onClick={onClick} return null;
disabled={processingRules} }
> return <>{options
{t(buttonType)} .map(([name, onClick]) => <div
</button>; 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"> 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>;

View File

@ -14,6 +14,7 @@ import IconTooltip from './IconTooltip';
const DomainCell = ({ const DomainCell = ({
answer_dnssec, answer_dnssec,
service_name,
client_proto, client_proto,
domain, domain,
time, time,
@ -49,6 +50,10 @@ const DomainCell = ({
protocol, protocol,
}; };
if (service_name) {
requestDetailsObj.check_service = service_name;
}
const sourceData = getSourceData(tracker); const sourceData = getSourceData(tracker);
const knownTrackerDataObj = { const knownTrackerDataObj = {
@ -98,7 +103,7 @@ const DomainCell = ({
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent} xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
place='bottom' /> place='bottom' />
<div className={valueClass}> <div className={valueClass}>
<div className="text-truncate" title={domain}>{domain}</div> <div className="text-truncate" title={domain}>{service_name || domain}</div>
{details && isDetailed {details && isDetailed
&& <div className="detailed-info d-none d-sm-block text-truncate" && <div className="detailed-info d-none d-sm-block text-truncate"
title={details}>{details}</div>} title={details}>{details}</div>}
@ -112,6 +117,7 @@ DomainCell.propTypes = {
domain: propTypes.string.isRequired, domain: propTypes.string.isRequired,
time: propTypes.string.isRequired, time: propTypes.string.isRequired,
type: propTypes.string.isRequired, type: propTypes.string.isRequired,
service_name: propTypes.string,
tracker: propTypes.object, tracker: propTypes.object,
}; };

View File

@ -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;

View 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,
};
};

View File

@ -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));

View File

@ -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);

View File

@ -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;

View File

@ -16,7 +16,11 @@
.card-table-overflow--limited { .card-table-overflow--limited {
overflow-y: auto; overflow-y: auto;
max-height: 280px; max-height: 17.5rem;
}
.card-table-overflow--limited.clients__table {
max-height: 18rem;
} }
.card-actions { .card-actions {

View File

@ -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>
); );

View File

@ -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;

View File

@ -97,7 +97,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
filterId, filterId,
rule, rule,
status, status,
serviceName: service_name, service_name,
originalAnswer: original_answer, originalAnswer: original_answer,
originalResponse: processResponse(original_answer), originalResponse: processResponse(original_answer),
tracker: getTrackerData(domain), tracker: getTrackerData(domain),
@ -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;
};