remove unused

This commit is contained in:
Ildar Kamalov 2025-07-21 17:26:37 +03:00
parent 66f5e215b4
commit dbd0429d30
31 changed files with 5 additions and 4089 deletions

View File

@ -5,7 +5,11 @@ import apiClient from '../api/Api';
import { normalizeLogs } from '../helpers/helpers';
import { DEFAULT_LOGS_FILTER, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';
import { addErrorToast, addSuccessToast } from './toasts';
import { SearchFormValues } from '../components/Logs';
type SearchFormValues = {
search: string;
response_status: string;
};
const getLogsWithParams = async (config: any) => {
const { older_than, filter, ...values } = config;

View File

@ -1,207 +0,0 @@
.nav-tabs .nav-link.active {
border-color: var(--btn-success-bgcolor);
color: var(--btn-success-bgcolor);
background: transparent;
}
.nav-tabs .nav-link.active:hover {
border-color: #4b9400;
color: #4b9400;
}
.nav-icon {
width: 15px;
height: 15px;
margin-right: 6px;
stroke: #9aa0ac;
}
.nav-tabs .nav-link.active .nav-icon,
.nav-tabs .nav-item.show .nav-icon {
stroke: var(--green-74);
}
.nav-tabs .nav-link.active:hover .nav-icon,
.nav-tabs .nav-item.show:hover .nav-icon {
stroke: #58a273;
}
.nav-tabs .nav-link {
width: 100%;
border: 0;
padding: 20px 0;
}
.header {
position: relative;
padding: 5px 0;
z-index: 102;
}
.mobile-menu {
position: fixed;
z-index: 103;
top: 0;
right: calc(100% + 5px);
display: block;
width: 250px;
height: 100vh;
transition: transform 0.3s ease;
background-color: var(--header-bgcolor);
overflow-y: auto;
}
.mobile-menu--active {
transform: translateX(255px);
box-shadow: 15px 0 50px rgba(0, 0, 0, 0.1);
}
.nav-tabs .nav-link--back {
height: 63px;
padding: 20px 0 21px;
font-weight: 600;
}
.nav-tabs .nav-link--account {
max-width: 160px;
font-size: 0.9rem;
text-overflow: ellipsis;
overflow: hidden;
}
.header-brand-img {
height: 24px;
}
.nav-tabs .nav-item.show .nav-link {
color: var(--green-74);
background-color: #fff;
border-bottom-color: var(--green-74);
}
.header__right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 100px;
}
.header__logout {
display: inline-flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
min-width: 25px;
padding: 2px;
margin-left: 10px;
color: #9aa0ac;
}
.header__logout:hover,
.header__logout:focus {
color: #6e7687;
}
.header__logout-icon {
height: 100%;
}
.header__row {
display: flex;
align-items: center;
}
.header__container {
width: 100%;
max-width: 1200px;
padding-right: 0.75rem;
padding-left: 0.75rem;
margin-right: auto;
margin-left: auto;
}
.header__column:last-child {
margin-left: auto;
}
.nav-tabs {
margin: 0;
}
@media screen and (min-width: 992px) {
.header {
padding: 0;
}
.nav-tabs .nav-link {
width: auto;
border-bottom: 1px solid transparent;
font-size: 13px;
white-space: nowrap;
}
.mobile-menu {
position: static;
display: flex;
padding: 0;
width: auto;
height: auto;
box-shadow: none;
overflow: initial;
}
.mobile-menu--active {
transform: none;
box-shadow: none;
}
.nav-icon {
display: none;
}
.nav-icon--gray {
color: #9aa0ac;
}
.nav-icon--white {
color: #fff;
}
.header-brand-img {
height: 32px;
}
.header__logout {
width: 35px;
height: 35px;
padding: 5px;
}
.header__row {
justify-content: space-between;
}
.header__column:last-child {
margin-left: 0;
}
.nav-tabs {
margin: 0 -0.75rem;
}
}
@media screen and (min-width: 1280px) {
.nav-tabs .nav-link {
font-size: 14px;
}
.nav-icon {
display: block;
}
}
.dns-status {
padding: 0.35em 0.5em;
line-height: 10px;
}

View File

@ -1,187 +0,0 @@
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames';
import { Trans, withTranslation } from 'react-i18next';
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
import Dropdown from '../ui/Dropdown';
const MENU_ITEMS = [
{
route: MENU_URLS.root,
exact: true,
icon: 'dashboard',
text: 'dashboard',
order: 0,
},
// Settings dropdown should have visual order 1
// Filters dropdown should have visual order 2
{
route: MENU_URLS.logs,
icon: 'log',
text: 'query_log',
order: 3,
},
{
route: MENU_URLS.guide,
icon: 'setup',
text: 'setup_guide',
order: 4,
},
];
const SETTINGS_ITEMS = [
{
route: SETTINGS_URLS.settings,
text: 'general_settings',
},
{
route: SETTINGS_URLS.dns,
text: 'dns_settings',
},
{
route: SETTINGS_URLS.encryption,
text: 'encryption_settings',
},
{
route: SETTINGS_URLS.clients,
text: 'client_settings',
},
{
route: SETTINGS_URLS.dhcp,
text: 'dhcp_settings',
},
];
const FILTERS_ITEMS = [
{
route: FILTERS_URLS.dns_blocklists,
text: 'dns_blocklists',
},
{
route: FILTERS_URLS.dns_allowlists,
text: 'dns_allowlists',
},
{
route: FILTERS_URLS.dns_rewrites,
text: 'dns_rewrites',
},
{
route: FILTERS_URLS.blocked_services,
text: 'blocked_services',
},
{
route: FILTERS_URLS.custom_rules,
text: 'custom_filtering_rules',
},
];
interface MenuProps {
isMenuOpen: boolean;
closeMenu: (...args: unknown[]) => unknown;
pathname: string;
t?: (...args: unknown[]) => string;
}
class Menu extends Component<MenuProps> {
handleClickOutside = () => {
this.props.closeMenu();
};
closeMenu = () => {
this.props.closeMenu();
};
getActiveClassForDropdown = (URLS: any) => {
const isActivePage = Object.values(URLS)
.some((item: any) => item === this.props.pathname);
return isActivePage ? 'active' : '';
};
getNavLink = ({ route, exact, text, order, className, icon }: any) => (
<NavLink
to={route}
key={route}
exact={exact || false}
className={`order-${order} ${className}`}
onClick={this.closeMenu}>
{icon && (
<svg className="nav-icon">
<use xlinkHref={`#${icon}`} />
</svg>
)}
<Trans>{text}</Trans>
</NavLink>
);
getDropdown = ({ label, order, URLS, icon, ITEMS }: any) => (
<Dropdown
label={this.props.t(label)}
baseClassName="dropdown"
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
icon={icon}>
{ITEMS.map((item: any) =>
this.getNavLink({
...item,
order,
className: 'dropdown-item',
}),
)}
</Dropdown>
);
render() {
const menuClass = classnames({
'header__column mobile-menu': true,
'mobile-menu--active': this.props.isMenuOpen,
});
return (
<>
<div className={menuClass}>
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
{MENU_ITEMS.map((item) => (
<li className={`nav-item order-${item.order}`} key={item.text} onClick={this.closeMenu}>
{this.getNavLink({
...item,
className: 'nav-link',
})}
</li>
))}
<li className="nav-item order-1">
{this.getDropdown({
order: 1,
label: 'settings',
icon: 'settings',
URLS: SETTINGS_URLS,
ITEMS: SETTINGS_ITEMS,
})}
</li>
<li className="nav-item order-2">
{this.getDropdown({
order: 2,
label: 'filters',
icon: 'filters',
URLS: FILTERS_URLS,
ITEMS: FILTERS_ITEMS,
})}
</li>
</ul>
</div>
</>
);
}
}
export default withTranslation()(enhanceWithClickOutside(Menu));

View File

@ -1,74 +0,0 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { shallowEqual, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import Menu from './Menu';
import { Logo } from '../ui/svg/logo';
import './Header.css';
import { RootState } from '../../initialState';
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const { protectionEnabled, processing, isCoreRunning, processingProfile, name } = useSelector(
(state: RootState) => state.dashboard,
shallowEqual,
);
const { pathname } = useLocation();
const toggleMenuOpen = () => {
setIsMenuOpen((isMenuOpen) => !isMenuOpen);
};
const closeMenu = () => {
setIsMenuOpen(false);
};
const badgeClass = classnames('badge dns-status', {
'badge-success': protectionEnabled,
'badge-danger': !protectionEnabled,
});
return (
<div className="header">
<div className="header__container">
<div className="header__row">
<div className="header-toggler d-lg-none ml-lg-0 collapsed" onClick={toggleMenuOpen}>
<span className="header-toggler-icon" />
</div>
<div className="header__column">
<div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1">
<Logo className="header-brand-img" />
</Link>
{!processing && isCoreRunning && (
<span className={badgeClass}>{t(protectionEnabled ? 'on' : 'off')}</span>
)}
</div>
</div>
<Menu pathname={pathname} isMenuOpen={isMenuOpen} closeMenu={closeMenu} />
<div className="header__column">
<div className="header__right">
{!processingProfile && name && (
<a href="control/logout" className="btn btn-sm btn-outline-secondary" data-testid="sign_out">
{t('sign_out')}
</a>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default Header;

View File

@ -1,21 +0,0 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link';
const AnonymizerNotification = () => (
<div className="alert alert-primary mt-6">
<Trans
components={[
<strong key="0">text</strong>,
<Link to="/settings#logs-config" key="1">
link
</Link>,
]}>
anonymizer_notification
</Trans>
</div>
);
export default AnonymizerNotification;

View File

@ -1,236 +0,0 @@
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 { Link, useHistory } from 'react-router-dom';
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
import { BLOCK_ACTIONS } from '../../../helpers/constants';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import IconTooltip from './IconTooltip';
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo } from './helpers';
import { getStats } from '../../../actions/stats';
import { updateLogs } from '../../../actions/queryLogs';
import { RootState } from '../../../initialState';
interface ClientCellProps {
client: string;
client_id?: string;
client_info?: {
name: string;
whois: {
country?: string;
city?: string;
orgname?: string;
};
disallowed: boolean;
disallowed_rule: string;
};
domain: string;
reason: string;
}
const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCellProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const history = useHistory();
const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);
const [isOptionsOpened, setOptionsOpened] = useState(false);
const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);
const clients = useSelector((state: RootState) => state.dashboard.clients);
const source = autoClient?.source;
const whoisAvailable = client_info && Object.keys(client_info.whois).length > 0;
const clientName = client_info?.name || client_id;
const clientInfo = client_info && {
...client_info,
whois_info: client_info?.whois,
name: clientName,
};
const id = nanoid();
const data = {
address: client,
name: clientName,
country: client_info?.whois?.country,
city: client_info?.whois?.city,
network: client_info?.whois?.orgname,
source_label: source,
};
const processedData = Object.entries(data);
const isFiltered = checkFiltered(reason);
const clientIds = clients.map((c: any) => c.ids).flat();
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
'mt-2': isDetailed && !client_info?.name && !whoisAvailable,
'white-space--nowrap': isDetailed,
});
const hintClass = classNames('icons mr-4 icon--24 logs__question icon--lightgray', {
'my-3': isDetailed,
});
const renderBlockingButton = (isFiltered: any, domain: any) => {
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const {
confirmMessage,
buttonKey: blockingClientKey,
lastRuleInAllowlist,
} = getBlockClientInfo(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
allowedClients,
);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
setOptionsOpened(false);
};
const BUTTON_OPTIONS = [
{
name: buttonType,
onClick,
className: isFiltered ? 'bg--green' : 'bg--danger',
},
{
name: blockingForClientKey,
onClick: () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
setOptionsOpened(false);
},
},
{
name: blockingClientKey,
onClick: async () => {
if (window.confirm(confirmMessage)) {
await dispatch(
toggleClientBlock(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
),
);
await dispatch(updateLogs());
setOptionsOpened(false);
}
},
disabled: lastRuleInAllowlist,
},
];
if (!clientIds.includes(client)) {
BUTTON_OPTIONS.push({
name: 'add_persistent_client',
onClick: () => {
history.push(`/#clients?clientId=${client}`);
},
});
}
const getOptions = (options: any) => {
if (options.length === 0) {
return null;
}
return (
<>
{options.map(({ name, onClick, disabled, className }: any) => (
<button
key={name}
className={classNames('button-action--arrow-option px-4 py-1', className)}
onClick={onClick}
disabled={disabled}>
{t(name)}
</button>
))}
</>
);
};
const content = getOptions(BUTTON_OPTIONS);
const containerClass = classNames('button-action__container', {
'button-action__container--detailed': isDetailed,
});
return (
<div className={containerClass}>
<button type="button" 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={content}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
defaultTooltipShown={true}
delayHide={0}
/>
)}
</div>
);
};
return (
<div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
<IconTooltip
className={hintClass}
columnClass="grid grid--limited"
tooltipClass="px-5 pb-5 pt-4"
xlinkHref="question"
contentItemClass="text-truncate key-colon o-hidden"
title="client_details"
content={processedData}
placement="bottom"
/>
<div className={nameClass}>
<div data-tip={true} data-for={id}>
{renderFormattedClientCell(client, clientInfo, isDetailed, true)}
</div>
{isDetailed && clientName && !whoisAvailable && (
<Link
className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
to={`logs?search="${encodeURIComponent(clientName)}"`}
title={clientName}>
{clientName}
</Link>
)}
</div>
{renderBlockingButton(isFiltered, domain)}
</div>
);
};
export default ClientCell;

View File

@ -1,37 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { formatDateTime, formatTime } from '../../../helpers/helpers';
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
import { RootState } from '../../../initialState';
interface DateCellProps {
time: string;
}
const DateCell = ({ time }: DateCellProps) => {
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
if (!time) {
return <></>;
}
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
return (
<div className="logs__cell logs__cell logs__cell--date text-truncate" role="gridcell">
<div className="logs__time" title={formattedTime}>
{formattedTime}
</div>
{isDetailed && (
<div className="detailed-info d-none d-sm-block text-truncate" title={formattedDate}>
{formattedDate}
</div>
)}
</div>
);
};
export default DateCell;

View File

@ -1,188 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import {
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
LONG_TIME_FORMAT,
SCHEME_TO_PROTOCOL_MAP,
} from '../../../helpers/constants';
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
import { getSourceData } from '../../../helpers/trackers/trackers';
import IconTooltip from './IconTooltip';
import { RootState } from '../../../initialState';
interface DomainCellProps {
answer_dnssec: boolean;
client_proto: string;
domain: string;
unicodeName?: string;
time: string;
type: string;
tracker?: {
name: string;
category: string;
};
ecs?: string;
}
const DomainCell = ({
answer_dnssec,
client_proto,
domain,
unicodeName,
time,
tracker,
type,
ecs,
}: DomainCellProps) => {
const { t } = useTranslation();
const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const hasTracker = !!tracker;
const lockIconClass = classNames('icons icon--24 d-none d-sm-block', {
'icon--green': answer_dnssec,
'icon--disabled': !answer_dnssec,
'my-3': isDetailed,
});
const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block logs__question', {
'icon--green': hasTracker,
'icon--disabled': !hasTracker,
'my-3': isDetailed,
});
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const ip = type ? `${t('type_table_header')}: ${type}` : '';
let requestDetailsObj: {
time_table_header: string;
date: string;
domain: string;
punycode?: string;
ecs?: string;
type_table_header?: string;
protocol?: string;
} = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
domain,
};
if (domain && unicodeName) {
requestDetailsObj = {
...requestDetailsObj,
domain: unicodeName,
punycode: domain,
};
}
if (ecs) {
requestDetailsObj = {
...requestDetailsObj,
ecs,
};
}
requestDetailsObj = {
...requestDetailsObj,
type_table_header: type,
protocol,
};
const sourceData = getSourceData(tracker);
const knownTrackerDataObj = {
name_table_header: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData && (
<a href={sourceData.url} target="_blank" rel="noopener noreferrer" className="link--green">
{sourceData.name}
</a>
),
};
const renderGrid = (content: any, idx: any) => {
const preparedContent = typeof content === 'string' ? t(content) : content;
const className = classNames('text-truncate o-hidden', { 'overflow-break': preparedContent?.length > 100 });
return (
<div key={idx} className={className}>
{preparedContent}
</div>
);
};
const getGrid = (contentObj: any, title: string, className?: string) => [
<div key={title} className={classNames('pb-2 grid--title', className)}>
{t(title)}
</div>,
<div key={`${title}-1`} className="grid grid--limited">
{React.Children.map(Object.entries(contentObj), renderGrid)}
</div>,
];
const requestDetails = getGrid(requestDetailsObj, 'request_details');
const renderContent = hasTracker
? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4'))
: requestDetails;
const valueClass = classNames('w-100 text-truncate', {
'px-2 d-flex justify-content-center flex-column': isDetailed,
});
const details = [ip, protocol].filter(Boolean).join(', ');
return (
<div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
{dnssec_enabled && (
<IconTooltip
className={lockIconClass}
tooltipClass="py-4 px-5 pb-45"
canShowTooltip={!!answer_dnssec}
xlinkHref="lock"
columnClass="w-100"
content="validated_with_dnssec"
placement="bottom"
/>
)}
<IconTooltip
className={privacyIconClass}
tooltipClass="pt-4 pb-5 px-5 mw-75"
xlinkHref="privacy"
contentItemClass="key-colon"
renderContent={renderContent}
placement="bottom"
/>
<div className={valueClass}>
{unicodeName ? (
<div className="text-truncate overflow-break-mobile" title={unicodeName}>
{unicodeName}
</div>
) : (
<div className="text-truncate overflow-break-mobile" title={domain}>
{domain}
</div>
)}
{details && isDetailed && (
<div className="detailed-info d-none d-sm-block text-truncate" title={details}>
{details}
</div>
)}
</div>
</div>
);
};
export default DomainCell;

View File

@ -1,73 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import { toggleDetailedLogs } from '../../../actions/queryLogs';
import HeaderCell from './HeaderCell';
import { RootState } from '../../../initialState';
const Header = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));
const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));
const HEADERS = [
{
className: 'logs__cell--date',
content: 'time_table_header',
},
{
className: 'logs__cell--domain',
content: 'request_table_header',
},
{
className: 'logs__cell--response',
content: 'response_table_header',
},
{
className: 'logs__cell--client',
content: (
<>
{t('client_table_header')}
{
<span>
<svg
className={classNames('icons icon--24 icon--green cursor--pointer mr-2', {
'icon--selected': !isDetailed,
})}
onClick={disableDetailedMode}>
<title>{t('compact')}</title>
<use xlinkHref="#list" />
</svg>
<svg
className={classNames('icons icon--24 icon--green cursor--pointer', {
'icon--selected': isDetailed,
})}
onClick={enableDetailedMode}>
<title>{t('default')}</title>
<use xlinkHref="#detailed_list" />
</svg>
</span>
}
</>
),
},
];
return (
<div className="logs__cell--header__container px-5" role="row">
{HEADERS.map(HeaderCell)}
</div>
);
};
export default Header;

View File

@ -1,23 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
interface HeaderCellProps {
content: string | React.ReactElement;
className?: string;
}
const HeaderCell = ({ content, className }: HeaderCellProps, idx: any) => {
const { t } = useTranslation();
return (
<div
key={idx}
className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}
role="columnheader">
{typeof content === 'string' ? t(content) : content}
</div>
);
};
export default HeaderCell;

View File

@ -1,148 +0,0 @@
.tooltip-custom__container {
min-width: 150px;
padding: 1rem 1.5rem 1.25rem 1.5rem;
font-size: 16px !important;
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 4px !important;
pointer-events: auto !important;
background-color: var(--ctrl-bgcolor);
color: var(--mcolor);
z-index: 102;
overflow-y: auto;
max-height: 100%;
}
.white-space--nowrap {
white-space: nowrap !important;
}
.overflow-break {
white-space: normal !important;
overflow-wrap: break-word;
}
@media (max-width: 991.98px) {
.overflow-break-mobile {
white-space: normal !important;
overflow-wrap: break-word;
}
}
.grid {
display: grid;
grid-template-columns: repeat(2, min-content);
grid-row-gap: 0.5rem;
grid-column-gap: 1rem;
}
.grid--limited {
grid-template-columns: repeat(2, minmax(0, min-content));
}
.grid--gap-bg {
grid-column-gap: 1.5rem;
}
.grid--title {
font-weight: 600;
}
.grid--title:not(:first-child) {
padding-top: 1rem;
}
@media (max-width: 1024px) {
.grid .title--border {
margin-bottom: 4px;
font-weight: 600;
}
.grid .key-colon {
margin-right: 4px;
color: var(--gray-8);
}
.grid__row {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 2px;
font-size: 14px;
word-break: break-all;
overflow: hidden;
}
.grid__row .filteringRules__filter,
.grid__row .filteringRules {
margin-bottom: 0;
}
}
@media (max-width: 767.98px) {
.grid {
grid-template-columns: 35% 55%;
}
.grid * {
grid-column: 1 / -1;
}
.grid > :nth-child(even) {
margin: -0.5rem 0 0;
}
.grid > .key__time_table_header,
.grid > .key__data,
.grid > .key__encryption_status,
.grid > .key__elapsed {
grid-column: 1 / span 1;
}
.grid > .value__time_table_header,
.grid > .value__data,
.grid > .value__encryption_status,
.grid > .value__elapsed {
grid-column: 2 / span 1;
margin: 0 !important;
}
}
.grid .key-colon:nth-child(odd)::after {
content: ':';
}
.grid__one-row {
grid-template-columns: 15rem;
}
.grid__flow-column {
grid-auto-flow: column;
}
.grid-content > * {
justify-content: space-between !important;
width: 100% !important;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
.title--border {
padding-top: 1rem;
}
.title--border:before {
content: '';
position: absolute;
left: 0;
border-top: 0.5px solid var(--gray-d8) !important;
width: 100%;
margin-top: -0.5rem;
}
.icon-cross {
position: absolute;
right: 0.5rem;
top: 0.5rem;
}

View File

@ -1,94 +0,0 @@
import React from 'react';
import { Trans } from 'react-i18next';
import classNames from 'classnames';
import PopperJS from 'popper.js';
import { TriggerTypes } from 'react-popper-tooltip';
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';
interface IconTooltipProps {
className?: string;
trigger?: TriggerTypes;
triggerClass?: string;
contentItemClass?: string;
columnClass?: string;
tooltipClass?: string;
title?: string;
placement?: PopperJS.Placement;
canShowTooltip?: boolean;
xlinkHref?: string;
content?: React.ReactNode;
renderContent?: React.ReactElement[];
onVisibilityChange?: (...args: unknown[]) => unknown;
defaultTooltipShown?: boolean;
delayHide?: number;
}
const IconTooltip = ({
className,
contentItemClass,
columnClass,
triggerClass,
canShowTooltip = true,
xlinkHref,
title,
placement,
tooltipClass,
content,
trigger,
onVisibilityChange,
defaultTooltipShown,
delayHide,
renderContent = content
? React.Children.map(
processContent(content),
(item, idx) => (
<div key={idx} className={contentItemClass}>
<Trans>{item || '—'}</Trans>
</div>
),
)
: null,
}: IconTooltipProps) => {
const tooltipContent = (
<>
{title && (
<div className="pb-4 h-25 grid-content font-weight-bold">
<Trans>{title}</Trans>
</div>
)}
<div className={classNames(columnClass)}>{renderContent}</div>
</>
);
const tooltipClassName = classNames('tooltip-custom__container', tooltipClass, { 'd-none': !canShowTooltip });
return (
<Tooltip
className={tooltipClassName}
content={tooltipContent}
placement={placement}
triggerClass={triggerClass}
trigger={trigger}
onVisibilityChange={onVisibilityChange}
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
delayHide={delayHide}
defaultTooltipShown={defaultTooltipShown}>
{xlinkHref && (
<svg className={className}>
<use xlinkHref={`#${xlinkHref}`} />
</svg>
)}
</Tooltip>
);
};
export default IconTooltip;

View File

@ -1,150 +0,0 @@
import { useTranslation } from 'react-i18next';
import { shallowEqual, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import { getRulesToFilterList, formatElapsedMs, getFilterNames, getServiceName } from '../../../helpers/helpers';
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
import IconTooltip from './IconTooltip';
import { RootState } from '../../../initialState';
interface ResponseCellProps {
elapsedMs: string;
originalResponse?: unknown[];
reason: string;
response: unknown[];
status: string;
upstream: string;
cached: boolean;
rules?: {
text: string;
filter_list_id: number;
}[];
service_name?: string;
}
const ResponseCell = ({
elapsedMs,
originalResponse,
reason,
response,
status,
upstream,
rules,
service_name,
cached,
}: ResponseCellProps) => {
const { t } = useTranslation();
const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const services = useSelector((store: RootState) => store?.services);
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isBlocked =
reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
const statusLabel = t(
isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason,
);
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
const renderResponses = (responseArr: any) => {
if (!responseArr || responseArr.length === 0) {
return '';
}
return (
<div>
{responseArr.map((response: any) => {
const className = classNames('white-space--nowrap', {
'overflow-break': response.length > 100,
});
return <div key={response} className={className}>{`${response}\n`}</div>;
})}
</div>
);
};
const COMMON_CONTENT = {
encryption_status: boldStatusLabel,
install_settings_dns: upstream,
...(cached && {
served_from_cache_label: (
<svg className="icons icon--20 icon--green mb-1">
<use xlinkHref="#check" />
</svg>
),
}),
elapsed: formattedElapsedMs,
response_code: status,
...(service_name &&
services.allServices && { service_name: getServiceName(services.allServices, service_name) }),
...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }),
response_table_header: renderResponses(response),
original_response: renderResponses(originalResponse),
};
const content =
rules.length > 0
? Object.entries(COMMON_CONTENT)
: Object.entries({
...COMMON_CONTENT,
filter: '',
});
const getDetailedInfo = (reason: any) => {
switch (reason) {
case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
if (!service_name || !services.allServices) {
return formattedElapsedMs;
}
return getServiceName(services.allServices, service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST:
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
default:
return formattedElapsedMs;
}
};
const detailedInfo = getDetailedInfo(reason);
return (
<div className="logs__cell logs__cell--response" role="gridcell">
<IconTooltip
className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}
columnClass="grid grid--limited"
tooltipClass="px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details"
contentItemClass="text-truncate key-colon o-hidden"
xlinkHref="question"
title="response_details"
content={content}
placement="bottom"
/>
<div className="text-truncate">
<div className="text-truncate" title={statusLabel}>
{statusLabel}
</div>
{isDetailed && (
<div className="detailed-info d-none d-sm-block pt-1 text-truncate" title={detailedInfo}>
{detailedInfo}
</div>
)}
</div>
</div>
);
};
export default ResponseCell;

View File

@ -1,25 +0,0 @@
import i18next from 'i18next';
export const BUTTON_PREFIX = 'btn_';
export const getBlockClientInfo = (ip: any, disallowed: any, disallowed_rule: any, allowedClients: any) => {
let confirmMessage;
if (disallowed) {
confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip });
} else {
confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`;
if (allowedClients.length > 0) {
confirmMessage = confirmMessage.concat(`\n\n${i18next.t('filter_allowlist', { disallowed_rule })}`);
}
}
const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule;
return {
confirmMessage,
buttonKey,
lastRuleInAllowlist,
};
};

View File

@ -1,306 +0,0 @@
import React, { Dispatch, memo, SetStateAction } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
captitalizeWords,
checkFiltered,
getRulesToFilterList,
formatDateTime,
formatElapsedMs,
formatTime,
getBlockingClientName,
getServiceName,
processContent,
} from '../../../helpers/helpers';
import {
BLOCK_ACTIONS,
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
FILTERED_STATUS,
FILTERED_STATUS_TO_META_MAP,
LONG_TIME_FORMAT,
QUERY_STATUS_COLORS,
SCHEME_TO_PROTOCOL_MAP,
} from '../../../helpers/constants';
import { getSourceData } from '../../../helpers/trackers/trackers';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import DateCell from './DateCell';
import DomainCell from './DomainCell';
import ResponseCell from './ResponseCell';
import ClientCell from './ClientCell';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
import { updateLogs } from '../../../actions/queryLogs';
import '../Logs.css';
import { RootState } from '../../../initialState';
interface RowProps {
style?: object;
rowProps: {
reason: string;
answer_dnssec: boolean;
client: string;
domain: string;
elapsedMs: string;
response: unknown[];
time: string;
tracker?: {
name: string;
category: string;
};
upstream: string;
cached: boolean;
type: string;
client_proto: string;
client_id?: string;
ecs?: string;
client_info?: {
name: string;
whois: {
country?: string;
city?: string;
orgname?: string;
};
disallowed: boolean;
disallowed_rule: string;
};
rules?: {
text: string;
filter_list_id: number;
}[];
originalResponse?: unknown[];
status: string;
service_name?: string;
};
isSmallScreen: boolean;
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
setButtonType: (...args: unknown[]) => unknown;
setModalOpened: (...args: unknown[]) => unknown;
}
const Row = memo(
({
style,
rowProps,
rowProps: { reason },
isSmallScreen,
setDetailedDataCurrent,
setButtonType,
setModalOpened,
}: RowProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);
const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);
const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual);
const processingSet = useSelector((state: RootState) => state.access.processingSet);
const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);
const services = useSelector((state: RootState) => state?.services);
const clients = useSelector((state: RootState) => state.dashboard.clients);
const onClick = () => {
if (!isSmallScreen) {
return;
}
const {
answer_dnssec,
client,
domain,
elapsedMs,
client_info,
response,
time,
tracker,
upstream,
type,
client_proto,
client_id,
rules,
originalResponse,
status,
service_name,
cached,
} = rowProps;
const hasTracker = !!tracker;
const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);
const source = autoClient?.source;
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isFiltered = checkFiltered(reason);
const isBlocked =
reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const onToggleBlock = () => {
dispatch(toggleBlocking(buttonType, domain));
};
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
const requestStatus = t(
isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason,
);
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const sourceData = getSourceData(tracker);
const {
confirmMessage,
buttonKey: blockingClientKey,
lastRuleInAllowlist,
} = getBlockClientInfo(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
allowedClients,
);
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 = async () => {
if (window.confirm(confirmMessage)) {
await dispatch(
toggleClientBlock(client, client_info?.disallowed || false, client_info?.disallowed_rule || ''),
);
await dispatch(updateLogs());
setModalOpened(false);
}
};
const blockButton = (
<>
<div className="title--border" />
<button
type="button"
className={classNames(
'button-action--arrow-option mb-1',
{ 'bg--danger': !isBlocked },
{ 'bg--green': isFiltered },
)}
onClick={onToggleBlock}>
{t(buttonType)}
</button>
</>
);
const blockForClientButton = (
<button
className="text-center font-weight-bold py-1 button-action--arrow-option"
onClick={onBlockingForClientClick}>
{t(blockingForClientKey)}
</button>
);
const blockClientButton = (
<button
className="text-center font-weight-bold py-1 button-action--arrow-option"
onClick={onBlockingClientClick}
disabled={processingSet || lastRuleInAllowlist}>
{t(blockingClientKey)}
</button>
);
const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: isBlocked ? <div className="bg--danger">{requestStatus}</div> : requestStatus,
...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE &&
service_name &&
services.allServices && { service_name: getServiceName(services.allServices, service_name) }),
domain,
type_table_header: type,
protocol,
known_tracker: hasTracker && 'title',
table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData && (
<a href={sourceData.url} target="_blank" rel="noopener noreferrer" className="link--green">
{sourceData.name}
</a>
),
response_details: 'title',
install_settings_dns: upstream,
...(cached && {
served_from_cache_label: (
<svg className="icons icon--20 icon--green">
<use xlinkHref="#check" />
</svg>
),
}),
elapsed: formattedElapsedMs,
...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }),
response_table_header: response?.join('\n'),
response_code: status,
client_details: 'title',
ip_address: client,
name: client_info?.name || client_id,
country: client_info?.whois?.country,
city: client_info?.whois?.city,
network: client_info?.whois?.orgname,
source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'),
[BUTTON_PREFIX + buttonType]: blockButton,
[BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
[BUTTON_PREFIX + blockingClientKey]: blockClientButton,
};
setDetailedDataCurrent(processContent(detailedData));
setButtonType(buttonType);
setModalOpened(true);
};
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const className = classNames(
'd-flex px-5 logs__row',
`logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`,
{
'logs__cell--detailed': isDetailed,
},
);
return (
<div style={style} className={className} onClick={onClick} role="row" data-testid="querylog_cell">
<DateCell {...rowProps} />
<DomainCell {...rowProps} />
<ResponseCell {...rowProps} />
<ClientCell {...rowProps} />
</div>
);
},
);
Row.displayName = 'Row';
export default Row;

View File

@ -1,31 +0,0 @@
import React, { Fragment } from 'react';
import { Trans } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link';
import Card from '../ui/Card';
const Disabled = () => (
<Fragment>
<div className="page-header">
<h1 className="page-title page-title--large">
<Trans>query_log</Trans>
</h1>
</div>
<Card>
<div className="lead text-center py-6">
<Trans
components={[
<Link to="/settings#logs-config" key="0">
link
</Link>,
]}>
query_log_disabled
</Trans>
</div>
</Card>
</Fragment>
);
export default Disabled;

View File

@ -1,111 +0,0 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { useFormContext } from 'react-hook-form';
import queryString from 'query-string';
import {
DEBOUNCE_FILTER_TIMEOUT,
DEFAULT_LOGS_FILTER,
RESPONSE_FILTER,
RESPONSE_FILTER_QUERIES,
} from '../../../helpers/constants';
import { setLogsFilter } from '../../../actions/queryLogs';
import useDebounce from '../../../helpers/useDebounce';
import { getLogsUrlParams } from '../../../helpers/helpers';
import { SearchField } from './SearchField';
import { SearchFormValues } from '..';
type Props = {
className?: string;
setIsLoading: (value: boolean) => void;
};
export const Form = ({ className, setIsLoading }: Props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const history = useHistory();
const { register, watch, setValue } = useFormContext<SearchFormValues>();
const searchValue = watch('search');
const responseStatusValue = watch('response_status');
const [debouncedSearch, setDebouncedSearch] = useDebounce(searchValue.trim(), DEBOUNCE_FILTER_TIMEOUT);
useEffect(() => {
dispatch(
setLogsFilter({
response_status: responseStatusValue,
search: debouncedSearch,
}),
);
history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);
}, [responseStatusValue, debouncedSearch]);
useEffect(() => {
if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {
setValue('response_status', DEFAULT_LOGS_FILTER.response_status);
}
}, [responseStatusValue, setValue]);
useEffect(() => {
const { search: searchUrlParam } = queryString.parse(history.location.search);
if (searchUrlParam !== searchValue) {
setValue('search', searchUrlParam ? searchUrlParam.toString() : '');
}
}, [history.location.search]);
const onInputClear = async () => {
setIsLoading(true);
history.push(getLogsUrlParams(DEFAULT_LOGS_FILTER.search, responseStatusValue));
setIsLoading(false);
};
const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setDebouncedSearch(searchValue);
}
};
return (
<form
className="d-flex flex-wrap form-control--container"
onSubmit={(e) => {
e.preventDefault();
}}>
<div className="field__search">
<SearchField
data-testid="querylog_search"
value={searchValue}
handleChange={(val) => setValue('search', val)}
onKeyDown={onEnterPress}
onClear={onInputClear}
placeholder={t('domain_or_client')}
tooltip={t('query_log_strict_search')}
className={classNames('form-control form-control--search form-control--transparent', className)}
/>
</div>
<div className="field__select">
<select
{...register('response_status')}
className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block">
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
<option key={LABEL} value={QUERY} disabled={disabled}>
{t(LABEL)}
</option>
))}
</select>
</div>
</form>
);
};

View File

@ -1,62 +0,0 @@
import React, { ComponentProps } from 'react';
import Tooltip from '../../ui/Tooltip';
interface Props extends ComponentProps<'input'> {
handleChange: (newValue: string) => void;
onClear: () => void;
tooltip?: string;
}
export const SearchField = ({
handleChange,
onClear,
value,
tooltip,
className,
...rest
}: Props) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(e.target.value);
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.value = e.target.value.trim();
handleChange(e.target.value)
}
return (
<>
<div className="input-group-search input-group-search__icon--magnifier">
<svg className="icons icon--24 icon--gray">
<use xlinkHref="#magnifier" />
</svg>
</div>
<input
className={className}
value={value}
onChange={handleInputChange}
onBlur={handleBlur}
{...rest}
/>
{typeof value === 'string' && value.length > 0 && (
<div
className="input-group-search input-group-search__icon--cross"
onClick={onClear}
>
<svg className="icons icon--20 icon--gray">
<use xlinkHref="#cross" />
</svg>
</div>
)}
{tooltip && (
<span className="input-group-search input-group-search__icon--tooltip">
<Tooltip content={tooltip} className="tooltip-container">
<svg className="icons icon--20 icon--gray">
<use xlinkHref="#question" />
</svg>
</Tooltip>
</span>
)}
</>
);
};

View File

@ -1,47 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Form } from './Form';
import { refreshFilteredLogs } from '../../../actions/queryLogs';
import { addSuccessToast } from '../../../actions/toasts';
interface FiltersProps {
processingGetLogs: boolean;
setIsLoading: (...args: unknown[]) => unknown;
}
const Filters = ({ setIsLoading }: FiltersProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const refreshLogs = async () => {
setIsLoading(true);
await dispatch(refreshFilteredLogs());
dispatch(addSuccessToast('query_log_updated'));
setIsLoading(false);
};
return (
<div className="page-header page-header--logs">
<h1 className="page-title page-title--large">
{t('query_log')}
<button
type="button"
className="btn btn-icon--green logs__refresh"
title={t('refresh_btn')}
onClick={refreshLogs}>
<svg className="icons icon--24">
<use xlinkHref="#update" />
</svg>
</button>
</h1>
<Form
setIsLoading={setIsLoading}
/>
</div>
);
};
export default Filters;

View File

@ -1,104 +0,0 @@
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import throttle from 'lodash/throttle';
import Loading from '../ui/Loading';
import Header from './Cells/Header';
import { getLogs } from '../../actions/queryLogs';
import Row from './Cells';
import { isScrolledIntoView } from '../../helpers/helpers';
import { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants';
import { RootState } from '../../initialState';
interface InfiniteTableProps {
isLoading: boolean;
items: unknown[];
isSmallScreen: boolean;
currentQuery: string;
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
setButtonType: (...args: unknown[]) => unknown;
setModalOpened: (...args: unknown[]) => unknown;
}
const InfiniteTable = ({
isLoading,
items,
isSmallScreen,
currentQuery,
setDetailedDataCurrent,
setButtonType,
setModalOpened,
}: InfiniteTableProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const loader = useRef(null);
const loadingRef = useRef(null);
const isEntireLog = useSelector((state: RootState) => state.queryLogs.isEntireLog);
const processingGetLogs = useSelector((state: RootState) => state.queryLogs.processingGetLogs);
const loading = isLoading || processingGetLogs;
const listener = useCallback(() => {
if (!loadingRef.current && loader.current && isScrolledIntoView(loader.current)) {
dispatch(getLogs(currentQuery));
}
}, []);
useEffect(() => {
loadingRef.current = processingGetLogs;
}, [processingGetLogs]);
useEffect(() => {
listener();
}, [items.length < QUERY_LOGS_PAGE_LIMIT, isEntireLog]);
useEffect(() => {
const THROTTLE_TIME = 100;
const throttledListener = throttle(listener, THROTTLE_TIME);
window.addEventListener('scroll', throttledListener);
return () => {
window.removeEventListener('scroll', throttledListener);
};
}, []);
const renderRow = (row: any, idx: any) => (
<Row
key={idx}
rowProps={row}
isSmallScreen={isSmallScreen}
setDetailedDataCurrent={setDetailedDataCurrent}
setButtonType={setButtonType}
setModalOpened={setModalOpened}
/>
);
const isNothingFound = items.length === 0 && !processingGetLogs;
return (
<div className="logs__table" role="grid">
{loading && <Loading />}
<Header />
{isNothingFound ? (
<label className="logs__no-data">{t('nothing_found')}</label>
) : (
<>
{items.map(renderRow)}
{!isEntireLog && (
<div ref={loader} className="logs__loading text-center">
{t('loading_table_status')}
</div>
)}
</>
)}
</div>
);
};
export default InfiniteTable;

View File

@ -1,538 +0,0 @@
:root {
--blue: #e5effd;
--green-pale: rgba(103, 178, 121, 0.1);
--red: rgba(223, 56, 18, 0.05);
--white: #fff;
--yellow: rgba(247, 181, 0, 0.1);
--size-date: 70;
--size-domain: 180;
--size-response: 150;
--size-client: 123;
--gray-216: rgba(216, 216, 216, 0.23);
--gray-4d: #4d4d4d;
--gray-f3: #f3f3f3;
--gray-8: #888;
--gray-3: #333;
--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;
}
[data-theme='dark'] {
--red: rgba(223, 56, 18, 0.25);
--green-pale: rgba(103, 178, 121, 0.25);
--yellow: rgba(247, 181, 0, 0.2);
}
.logs__text {
padding: 0 1px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 1rem;
font-family: var(--font-family-sans-serif);
color: var(--logs__text-color);
letter-spacing: 0;
line-height: 1.5rem;
}
[data-theme='dark'] .logs__text a {
color: var(--gray-f3);
}
[data-theme='dark'] .logs__text a:hover {
color: var(--gray-f3);
}
.logs__text--bold {
font-weight: 600;
}
.logs__time {
font-size: 1rem;
line-height: 1.5;
}
.detailed-info {
font-size: 0.8rem;
line-height: 1.4;
color: var(--detailed-info-color);
}
.logs__text--link {
color: #467fcf;
}
.logs__text--link:hover,
.logs__text--link:focus {
color: #295a9f;
}
[data-theme='dark'] .logs__text--link,
[data-theme='dark'] .logs__text--link:hover,
[data-theme='dark'] .logs__text--link:focus {
color: var(--gray-f3);
}
.logs__table .logs__text--client {
padding-right: 32px;
}
.icon--selected {
background-color: var(--gray-f3);
border: solid 1px var(--gray-d8);
border-radius: 4px;
}
[data-theme='dark'] .icon--selected {
opacity: 0.75;
}
.text-pre {
white-space: pre-wrap !important;
overflow-wrap: break-word;
overflow: visible;
}
.link--green {
color: var(--green79);
}
.w-90 {
max-width: 90% !important;
}
.pb-45 {
padding-bottom: 1.25rem !important;
}
.mh-100 {
max-height: 100% !important;
}
.icon24 {
width: 24px;
height: 24px;
}
.icon12 {
width: 12px;
height: 12px;
}
.cursor--pointer {
cursor: pointer;
}
.custom-select__arrow--left {
background: var(--white) url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K") no-repeat;
background-position: 5px 9px;
background-size: 22px;
}
.custom-select--logs {
padding: 0.5rem 0.75rem 0.5rem 2rem !important;
}
.form-control--search {
padding: 0 2.5rem;
height: 2.25rem;
flex-grow: 1;
}
.form-control--transparent {
background-color: transparent !important;
}
[data-theme='dark'] .form-control--transparent option {
background-color: var(--card-bgcolor);
}
.input-group-search {
background-color: transparent;
position: relative;
width: 1.5rem;
height: 1.5rem;
top: 0.4rem;
}
.input-group-search__icon--magnifier {
left: 2rem;
}
.input-group-search__icon--cross {
left: -4.5rem;
cursor: pointer;
}
.input-group-search__icon--tooltip {
left: -4rem;
}
.form-control--container {
flex: auto;
}
.field__search {
display: flex;
flex-grow: 1;
}
@media (max-width: 767.98px) {
.form-control--container {
width: 100%;
flex-direction: column;
}
.form-control--search {
width: 100%;
}
.field__select {
margin-top: 1.5rem;
padding-left: 24px;
padding-right: 24px;
}
}
@media screen and (max-width: 767.98px) {
.logs__table .logs__cell--response,
.logs__table .logs__cell--client {
display: none !important;
}
}
.logs__refresh {
position: relative;
top: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin-left: 0.9375rem;
background-color: transparent;
}
.logs__cell {
padding: 1rem 1rem 0.5rem 0;
}
.logs__cell--date {
width: 4.375rem;
flex: var(--size-date) 0 auto;
}
.logs__cell--domain {
width: 11.25rem;
flex: var(--size-domain) 0 auto;
}
.logs__cell--response {
width: 9.375rem;
flex: var(--size-response) 0 auto;
}
.logs__cell--client {
width: 7.6875rem;
flex: var(--size-client) 0 auto;
padding-right: 0;
position: relative;
}
@media screen and (min-width: 1025px) {
.logs__cell--client {
width: 13rem;
}
}
.logs__cell--header__container > .logs__cell--header__item {
border-right: 0;
font-size: 1rem;
}
.logs__cell--header__container > .logs__cell--header__item:last-child {
padding-right: 0;
}
.button-action__container {
display: flex;
position: absolute;
right: 2px;
bottom: 0.5rem;
}
@media screen and (max-width: 1024px) {
.button-action__container {
display: none;
}
}
.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--small {
width: fit-content;
}
.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:hover {
cursor: pointer;
}
.button-action--arrow-option {
background: transparent;
border: 0;
display: block;
width: 100%;
padding-top: 0.2rem;
padding-bottom: 0.2rem;
text-align: center;
font-weight: 700;
color: inherit;
cursor: pointer;
}
.button-action--arrow-option:hover,
.button-action--arrow-option:focus {
outline: none;
}
.button-action--arrow-option:focus-visible {
outline: 2px solid #295a9f;
}
.button-action--arrow-option:disabled {
display: none;
}
.tooltip-custom__container .button-action--arrow-option {
padding-bottom: 0;
text-align: left;
font-weight: 400;
}
.tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {
background: var(--gray-f3);
overflow: hidden;
}
[data-theme='dark'] .tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {
background: var(--ctrl-dropdown-bgcolor-focus);
}
.button-action--arrow-option-container {
overflow: visible;
transform-origin: left;
padding: 1rem 0;
}
.logs__row {
position: relative;
display: flex;
min-height: 26px;
overflow: hidden;
text-overflow: ellipsis;
}
.logs__row--icons {
flex-wrap: wrap;
}
.logs__table .logs__row {
border-bottom: 2px solid var(--gray-216);
}
.logs__tag {
text-overflow: ellipsis;
overflow: hidden;
}
/* QUERY_STATUS_COLORS */
.logs__row--blue {
background-color: var(--logs__row--blue-bgcolor);
}
[data-theme='dark'] .logs__row--blue .logs__text--link {
color: var(--white);
}
.logs__row--green {
background-color: var(--green-pale);
}
.logs__row--red {
background-color: var(--red) !important;
}
.logs__row--white {
background-color: var(--logs__row--white-bgcolor);
}
.logs__row--yellow {
background-color: var(--yellow);
}
.logs__no-data {
color: var(--mcolor);
background-color: var(--logs__table-bgcolor);
pointer-events: none;
font-weight: 600;
text-align: center;
padding-top: 21rem;
display: block;
}
.logs__loading {
padding: 1rem 0;
}
.logs__table {
background-color: var(--logs__table-bgcolor);
border: 0;
border-radius: 8px;
min-height: 43rem;
max-width: 100%;
align-items: stretch;
width: 100%;
border-collapse: collapse;
contain: layout;
overflow-x: hidden;
overflow-y: auto;
will-change: scroll-position;
}
.logs__table .logs__cell--response,
.logs__table .logs__cell--client {
display: flex;
}
.logs__cell--header__container {
display: flex;
}
.logs__table > .logs__cell--header__container > .logs__cell--client {
display: flex;
justify-content: space-between;
}
.logs__table .loading:after {
top: 10%;
}
.logs__table .loading:before {
min-height: 100%;
}
.logs__whois {
display: inline;
font-size: 12px;
white-space: nowrap;
}
.logs__whois::after {
content: '|';
padding: 0 5px;
opacity: 0.3;
}
.logs__whois:last-child::after {
content: '';
}
.logs__whois-icon.icons {
position: relative;
top: -2px;
width: 12px;
height: 12px;
margin-right: 1px;
opacity: 0.5;
}
.filteringRules__rule {
margin-bottom: 0;
}
.filteringRules__filter {
font-style: italic;
font-weight: 400;
margin-bottom: 1rem;
}
.bg--danger {
color: var(--danger);
}
.bg--green {
color: var(--green79);
}
[data-theme='dark'] .logs__question.icon--lightgray {
color: var(--gray-f3);
}
@media (max-width: 1024px) {
.logs__question {
display: none;
}
}
.logs__modal {
max-width: 720px;
}
.logs__modal-wrap {
padding: 1rem 1.5rem;
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

@ -1,255 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Trans } from 'react-i18next';
import Modal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import queryString from 'query-string';
import classNames from 'classnames';
import { FormProvider, useForm } from 'react-hook-form';
import { BLOCK_ACTIONS, DEFAULT_LOGS_FILTER, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';
import Loading from '../ui/Loading';
import Filters from './Filters';
import Disabled from './Disabled';
import { getFilteringStatus } from '../../actions/filtering';
import { getClients } from '../../actions';
import { getDnsConfig } from '../../actions/dnsConfig';
import { getAccessList } from '../../actions/access';
import { getAllBlockedServices } from '../../actions/services';
import { getLogsConfig, resetFilteredLogs, setFilteredLogs, toggleDetailedLogs } from '../../actions/queryLogs';
import InfiniteTable from './InfiniteTable';
import './Logs.css';
import { BUTTON_PREFIX } from './Cells/helpers';
import AnonymizerNotification from './AnonymizerNotification';
import { RootState } from '../../initialState';
export type SearchFormValues = {
search: string;
response_status: string;
};
const processContent = (data: any, _buttonType: string) =>
Object.entries(data).map(([key, value]) => {
if (!value) {
return null;
}
const isTitle = value === 'title';
const isButton = key.startsWith(BUTTON_PREFIX);
const isBoolean = typeof value === 'boolean';
const isHidden = isBoolean && value === false;
let keyClass = 'key-colon';
if (isTitle) {
keyClass = 'title--border';
}
if (isButton || isBoolean) {
keyClass = '';
}
return isHidden ? null : (
<div className="grid__row" key={key}>
<div
className={classNames(`key__${key}`, keyClass, {
'font-weight-bold': isBoolean && value === true,
})}>
<Trans>{isButton ? value : key}</Trans>
</div>
<div className={`value__${key} text-pre text-truncate`}>
<Trans>{isTitle || isButton || isBoolean ? '' : value || '—'}</Trans>
</div>
</div>
);
});
const Logs = () => {
const dispatch = useDispatch();
const history = useHistory();
const { response_status: response_status_url_param, search: search_url_param } = queryString.parse(
history.location.search,
);
const {
enabled,
processingGetConfig,
processingGetLogs,
anonymize_client_ip: anonymizeClientIp,
} = useSelector((state: RootState) => state.queryLogs, shallowEqual);
const filter = useSelector((state: RootState) => state.queryLogs.filter, shallowEqual);
const logs = useSelector((state: RootState) => state.queryLogs.logs, shallowEqual);
const search = search_url_param || filter?.search || '';
const response_status = response_status_url_param || filter?.response_status || '';
const formMethods = useForm<SearchFormValues>({
mode: 'onBlur',
defaultValues: {
search: search || DEFAULT_LOGS_FILTER.search,
response_status: response_status || DEFAULT_LOGS_FILTER.response_status,
},
});
const { watch } = formMethods;
const currentQuery = watch('search');
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth <= MEDIUM_SCREEN_SIZE);
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
const [isModalOpened, setModalOpened] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const closeModal = () => setModalOpened(false);
useEffect(() => {
(async () => {
setIsLoading(true);
await dispatch(
setFilteredLogs({
search,
response_status,
}),
);
setIsLoading(false);
})();
}, [response_status, search]);
const mediaQuery = window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`);
const mediaQueryHandler = (e: any) => {
setIsSmallScreen(e.matches);
if (e.matches) {
dispatch(toggleDetailedLogs(false));
} else {
dispatch(toggleDetailedLogs(true));
}
};
useEffect(() => {
try {
mediaQuery.addEventListener('change', mediaQueryHandler);
} catch (e1) {
try {
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
mediaQuery.addListener(mediaQueryHandler);
} catch (e2) {
console.error(e2);
}
}
(async () => {
setIsLoading(true);
dispatch(getFilteringStatus());
dispatch(getClients());
dispatch(getAllBlockedServices());
try {
await Promise.all([dispatch(getLogsConfig()), dispatch(getDnsConfig()), dispatch(getAccessList())]);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
})();
return () => {
try {
mediaQuery.removeEventListener('change', mediaQueryHandler);
} catch (e1) {
try {
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
mediaQuery.removeListener(mediaQueryHandler);
} catch (e2) {
console.error(e2);
}
}
dispatch(resetFilteredLogs());
};
}, []);
useEffect(() => {
if (!history.location.search) {
(async () => {
setIsLoading(true);
await dispatch(setFilteredLogs());
setIsLoading(false);
})();
}
}, [history.location.search]);
const renderPage = () => (
<>
<FormProvider {...formMethods}>
<Filters
setIsLoading={setIsLoading}
processingGetLogs={processingGetLogs}
/>
</FormProvider>
<InfiniteTable
isLoading={isLoading}
items={logs}
isSmallScreen={isSmallScreen}
setDetailedDataCurrent={setDetailedDataCurrent}
setButtonType={setButtonType}
setModalOpened={setModalOpened}
currentQuery={currentQuery}
/>
<Modal
portalClassName="grid"
isOpen={isSmallScreen && isModalOpened}
onRequestClose={closeModal}
style={{
content: {
width: 'calc(100% - 32px)',
height: 'fit-content',
left: '50%',
top: 47,
padding: '0',
maxWidth: '720px',
transform: 'translateX(-50%)',
},
overlay: {
backgroundColor: 'rgba(0,0,0,0.5)',
},
}}>
<div className="logs__modal-wrap">
<svg className="icon icon--24 icon-cross d-block cursor--pointer" onClick={closeModal}>
<use xlinkHref="#cross" />
</svg>
{processContent(detailedDataCurrent, buttonType)}
</div>
</Modal>
</>
);
return (
<>
{enabled && (
<>
{processingGetConfig && <Loading />}
{anonymizeClientIp && <AnonymizerNotification />}
{!processingGetConfig && renderPage()}
</>
)}
{!enabled && !processingGetConfig && <Disabled />}
</>
);
};
export default Logs;

View File

@ -1,115 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { toNumber } from '../../../helpers/form';
import { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';
import { Checkbox } from '../../ui/Controls/Checkbox';
import { Select } from '../../ui/Controls/Select';
const THREE_DAYS_INTERVAL = DAY * 3;
const SEVEN_DAYS_INTERVAL = DAY * 7;
const getTitleForInterval = (interval: number) => {
if (interval === 0) {
return i18next.t('disabled');
}
if (interval === THREE_DAYS_INTERVAL || interval === SEVEN_DAYS_INTERVAL) {
return i18next.t('interval_days', { count: interval / DAY });
}
return i18next.t('interval_hours', { count: interval });
};
export type FormValues = {
enabled: boolean;
interval: number;
};
type Props = {
initialValues: FormValues;
setFiltersConfig: (values: FormValues) => void;
processing: boolean;
};
export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: Props) => {
const { t } = useTranslation();
const prevFormValuesRef = useRef<FormValues>(initialValues);
const { watch, control } = useForm({
mode: 'onBlur',
defaultValues: initialValues,
});
const formValues = watch();
useEffect(() => {
const prevFormValues = prevFormValuesRef.current;
if (JSON.stringify(prevFormValues) !== JSON.stringify(formValues)) {
setFiltersConfig(formValues);
prevFormValuesRef.current = formValues;
}
}, [formValues]);
const components = {
a: <a href={FILTERS_RELATIVE_LINK} rel="noopener noreferrer" />,
};
return (
<>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<Controller
name="enabled"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="filters_enabled"
title={t('block_domain_use_filters_and_hosts')}
disabled={processing}
/>
)}
/>
<p>
<Trans components={components}>filters_block_toggle_hint</Trans>
</p>
</div>
</div>
<div className="col-12 col-md-5">
<div className="form__group form__group--inner mb-5">
<label className="form__label">
<Trans>filters_interval</Trans>
</label>
<Controller
name="interval"
control={control}
render={({ field }) => (
<Select
{...field}
data-testid="filters_interval"
disabled={processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}>
{FILTERS_INTERVALS_HOURS.map((interval) => (
<option value={interval} key={interval}>
{getTitleForInterval(interval)}
</option>
))}
</Select>
)}
/>
</div>
</div>
</div>
</>
);
};

View File

@ -1,11 +0,0 @@
.form__button {
margin-left: 1.5rem;
}
@media (max-width: 500px) {
.form__button {
margin-left: 0;
margin-top: 1rem;
display: block;
}
}

View File

@ -1,229 +0,0 @@
import React, { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { Controller, useForm } from 'react-hook-form';
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
import { QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
import '../FormButton.css';
import { Checkbox } from '../../ui/Controls/Checkbox';
import { Input } from '../../ui/Controls/Input';
import { toNumber } from '../../../helpers/form';
import { Textarea } from '../../ui/Controls/Textarea';
const getIntervalTitle = (interval: number) => {
switch (interval) {
case RETENTION_CUSTOM:
return i18next.t('settings_custom');
case 6 * HOUR:
return i18next.t('interval_6_hour');
case DAY:
return i18next.t('interval_24_hour');
default:
return i18next.t('interval_days', { count: interval / DAY });
}
};
export type FormValues = {
enabled: boolean;
anonymize_client_ip: boolean;
interval: number;
customInterval?: number | null;
ignored: string;
};
type Props = {
initialValues: Partial<FormValues>;
processing: boolean;
processingReset: boolean;
onSubmit: (values: FormValues) => void;
onReset: () => void;
};
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
const { t } = useTranslation();
const {
handleSubmit,
watch,
setValue,
control,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onBlur',
defaultValues: {
enabled: initialValues.enabled || false,
anonymize_client_ip: initialValues.anonymize_client_ip || false,
interval: initialValues.interval || DAY,
customInterval: initialValues.customInterval || null,
ignored: initialValues.ignored || '',
},
});
const intervalValue = watch('interval');
const customIntervalValue = watch('customInterval');
useEffect(() => {
if (QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)) {
setValue('customInterval', null);
}
}, [intervalValue]);
const onSubmitForm = (data: FormValues) => {
onSubmit(data);
};
const handleIgnoredBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const trimmed = trimLinesAndRemoveEmpty(e.target.value);
setValue('ignored', trimmed);
};
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
return (
<form onSubmit={handleSubmit(onSubmitForm)}>
<div className="form__group form__group--settings">
<Controller
name="enabled"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="logs_enabled"
title={t('query_log_enable')}
disabled={processing}
/>
)}
/>
</div>
<div className="form__group form__group--settings">
<Controller
name="anonymize_client_ip"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="logs_anonymize_client_ip"
title={t('anonymize_client_ip')}
subtitle={t('anonymize_client_ip_desc')}
disabled={processing}
/>
)}
/>
</div>
<div className="form__label">
<Trans>query_log_retention</Trans>
</div>
<div className="form__group form__group--settings">
<div className="custom-controls-stacked">
<label className="custom-control custom-radio">
<input
type="radio"
data-testid="logs_config_interval"
className="custom-control-input"
disabled={processing}
checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
value={RETENTION_CUSTOM}
onChange={(e) => {
setValue('interval', parseInt(e.target.value, 10));
}}
/>
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
</label>
{!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
<Controller
name="customInterval"
control={control}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="logs_config_custom_interval"
disabled={processing}
error={fieldState.error?.message}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
)}
{QUERY_LOG_INTERVALS_DAYS.map((interval) => (
<label key={interval} className="custom-control custom-radio">
<input
type="radio"
className="custom-control-input"
data-testid={`logs_config_${interval}`}
disabled={processing}
value={interval}
checked={intervalValue === interval}
onChange={(e) => {
setValue('interval', parseInt(e.target.value, 10));
}}
/>
<span className="custom-control-label">{getIntervalTitle(interval)}</span>
</label>
))}
</div>
</div>
<label className="form__label form__label--with-desc">
<Trans>ignore_domains_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>ignore_domains_desc_query</Trans>
</div>
<div className="form__group form__group--settings">
<Controller
name="ignored"
control={control}
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="logs_config_ingored"
placeholder={t('ignore_domains')}
className="text-input"
disabled={processing}
error={fieldState.error?.message}
onBlur={handleIgnoredBlur}
/>
)}
/>
</div>
<div className="mt-5">
<button
type="submit"
data-testid="logs_config_save"
className="btn btn-success btn-standard btn-large"
disabled={disableSubmit}>
<Trans>save_btn</Trans>
</button>
<button
type="button"
data-testid="logs_config_clear"
className="btn btn-outline-secondary btn-standard form__button"
onClick={onReset}
disabled={processingReset}>
<Trans>query_log_clear</Trans>
</button>
</div>
</form>
);
};

View File

@ -1,87 +0,0 @@
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card';
import { Form, FormValues } from './Form';
import { HOUR } from '../../../helpers/constants';
interface LogsConfigProps {
interval: number;
customInterval?: number;
enabled: boolean;
anonymize_client_ip: boolean;
processing: boolean;
ignored: unknown[];
processingClear: boolean;
setLogsConfig: (...args: unknown[]) => unknown;
clearLogs: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class LogsConfig extends Component<LogsConfigProps> {
handleFormSubmit = (values: FormValues) => {
const { t, interval: prevInterval } = this.props;
const { interval, customInterval, ...rest } = values;
const newInterval = customInterval ? customInterval * HOUR : interval;
const data = {
...rest,
ignored: values.ignored ? values.ignored.split('\n') : [],
interval: newInterval,
};
if (newInterval < prevInterval) {
// eslint-disable-next-line no-alert
if (window.confirm(t('query_log_retention_confirm'))) {
this.props.setLogsConfig(data);
}
} else {
this.props.setLogsConfig(data);
}
};
handleClear = () => {
const { t, clearLogs } = this.props;
// eslint-disable-next-line no-alert
if (window.confirm(t('query_log_confirm_clear'))) {
clearLogs();
}
};
render() {
const {
t,
enabled,
interval,
processing,
processingClear,
anonymize_client_ip,
ignored,
customInterval,
} = this.props;
return (
<Card title={t('query_log_configuration')} bodyType="card-body box-body--settings" id="logs-config">
<div className="form">
<Form
initialValues={{
enabled,
interval,
customInterval,
anonymize_client_ip,
ignored: ignored?.join('\n'),
}}
processing={processing}
processingReset={processingClear}
onSubmit={this.handleFormSubmit}
onReset={this.handleClear}
/>
</div>
</Card>
);
}
}
export default withTranslation()(LogsConfig);

View File

@ -1,171 +0,0 @@
.form__group {
position: relative;
margin-bottom: 15px;
}
.form__group:last-child {
margin-bottom: 0;
}
.form__group--settings:last-child {
margin-bottom: 20px;
}
.form__group--inner {
max-width: 300px;
margin-top: -10px;
margin-left: 40px;
font-size: 14px;
}
.form__group--input {
max-width: 300px;
margin: 0 1.5rem 10px;
}
.form__group--checkbox {
margin-bottom: 25px;
}
.form__group--inner .form__group--checkbox {
margin-bottom: 12px;
}
.form__group--inner .form__group--checkbox:last-child {
margin-bottom: 0;
}
.form__inline {
display: flex;
justify-content: flex-start;
}
.btn-standard {
padding-left: 20px;
padding-right: 20px;
}
.btn-large {
min-width: 150px;
}
.form-control--textarea {
min-height: 110px;
transition: 0.3s ease-in-out background-color;
}
.form-control--textarea-small {
min-height: 90px;
}
.form-control--textarea-large {
min-height: 240px;
}
.form__message {
margin-top: 4px;
font-size: 11px;
}
.form__message--error {
color: #cd201f;
}
.form__message--left-pad {
padding-left: 0.85rem;
}
.interface__title {
font-size: 13px;
font-weight: 700;
}
.interface__ip:after {
content: ', ';
}
.interface__ip:last-child:after {
content: '';
}
.form__desc {
margin-top: 10px;
font-size: 13px;
color: var(--scolor);
}
.form__desc--top {
margin: 0 0 8px;
}
.form__label {
margin-bottom: 8px;
}
.form__label--bold {
font-weight: 700;
}
.form__label--with-desc {
margin-bottom: 0;
}
.form__label--bot {
margin-bottom: 10px;
}
.form__label--top {
margin-top: 10px;
}
.form__status {
margin-top: 10px;
font-size: 14px;
line-height: 1.7;
}
.encryption__list {
padding-left: 0;
}
.encryption__list li {
list-style: inside;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: transparent;
overflow: hidden;
}
.btn-icon--green {
color: var(--green);
}
.btn-icon-sm {
width: 23px;
height: 23px;
min-width: 23px;
padding: 5px;
}
.custom-control-label,
.custom-control-label:before {
transition:
0.3s ease-in-out background-color,
0.3s ease-in-out color;
}
.custom-select:disabled {
background-color: #f9f9f9;
}
.custom-select {
transition:
0.3s ease-in-out background-color,
0.3s ease-in-out color;
}

View File

@ -1,211 +0,0 @@
import React, { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { Controller, useForm } from 'react-hook-form';
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
import '../FormButton.css';
import { Checkbox } from '../../ui/Controls/Checkbox';
import { Input } from '../../ui/Controls/Input';
import { toNumber } from '../../../helpers/form';
import { Textarea } from '../../ui/Controls/Textarea';
const getIntervalTitle = (interval: any) => {
switch (interval) {
case RETENTION_CUSTOM:
return i18next.t('settings_custom');
case DAY:
return i18next.t('interval_24_hour');
default:
return i18next.t('interval_days', { count: interval / DAY });
}
};
export type FormValues = {
enabled: boolean;
interval: number;
customInterval?: number | null;
ignored: string;
};
const defaultFormValues = {
enabled: false,
interval: DAY,
customInterval: null,
ignored: '',
};
type Props = {
initialValues: FormValues;
processing: boolean;
processingReset: boolean;
onSubmit: (values: FormValues) => void;
onReset: () => void;
};
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
const { t } = useTranslation();
const {
handleSubmit,
watch,
setValue,
control,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onBlur',
defaultValues: {
...defaultFormValues,
...initialValues,
},
});
const intervalValue = watch('interval');
const customIntervalValue = watch('customInterval');
useEffect(() => {
if (STATS_INTERVALS_DAYS.includes(intervalValue)) {
setValue('customInterval', null);
}
}, [intervalValue]);
const onSubmitForm = (data: FormValues) => {
onSubmit(data);
};
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
return (
<form onSubmit={handleSubmit(onSubmitForm)}>
<div className="form__group form__group--settings">
<Controller
name="enabled"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="stats_config_enabled"
title={t('statistics_enable')}
disabled={processing}
/>
)}
/>
</div>
<div className="form__label form__label--with-desc">
<Trans>statistics_retention</Trans>
</div>
<div className="form__desc form__desc--top">
<Trans>statistics_retention_desc</Trans>
</div>
<div className="form__group form__group--settings mt-2">
<div className="custom-controls-stacked">
<label className="custom-control custom-radio">
<input
type="radio"
data-testid="stats_config_interval"
className="custom-control-input"
disabled={processing}
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
value={RETENTION_CUSTOM}
onChange={(e) => {
setValue('interval', parseInt(e.target.value, 10));
}}
/>
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
</label>
{!STATS_INTERVALS_DAYS.includes(intervalValue) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">{i18next.t('custom_retention_input')}</div>
<Controller
name="customInterval"
control={control}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="stats_config_custom_interval"
disabled={processing}
error={fieldState.error?.message}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
)}
{STATS_INTERVALS_DAYS.map((interval) => (
<label key={interval} className="custom-control custom-radio">
<input
type="radio"
className="custom-control-input"
disabled={processing}
value={interval}
checked={intervalValue === interval}
onChange={(e) => {
setValue('interval', parseInt(e.target.value, 10));
}}
/>
<span className="custom-control-label">{getIntervalTitle(interval)}</span>
</label>
))}
</div>
</div>
<div className="form__label form__label--with-desc">
<Trans>ignore_domains_title</Trans>
</div>
<div className="form__desc form__desc--top">
<Trans>ignore_domains_desc_stats</Trans>
</div>
<div className="form__group form__group--settings">
<Controller
name="ignored"
control={control}
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="stats_config_ignored"
placeholder={t('ignore_domains')}
className="text-input"
disabled={processing}
error={fieldState.error?.message}
trimOnBlur
/>
)}
/>
</div>
<div className="mt-5">
<button
type="submit"
data-testid="stats_config_save"
className="btn btn-success btn-standard btn-large"
disabled={disableSubmit}>
<Trans>save_btn</Trans>
</button>
<button
type="button"
data-testid="stats_config_clear"
className="btn btn-outline-secondary btn-standard form__button"
onClick={onReset}
disabled={processingReset}>
<Trans>statistics_clear</Trans>
</button>
</div>
</form>
);
};

View File

@ -1,81 +0,0 @@
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card';
import { Form, FormValues } from './Form';
import { HOUR } from '../../../helpers/constants';
interface StatsConfigProps {
interval: number;
customInterval?: number;
ignored: unknown[];
enabled: boolean;
processing: boolean;
processingReset: boolean;
setStatsConfig: (...args: unknown[]) => unknown;
resetStats: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class StatsConfig extends Component<StatsConfigProps> {
handleFormSubmit = ({ enabled, interval, ignored, customInterval }: FormValues) => {
const { t, interval: prevInterval } = this.props;
const newInterval = customInterval ? customInterval * HOUR : interval;
const config = {
enabled,
interval: newInterval,
ignored: ignored ? ignored.split('\n') : [],
};
if (config.interval < prevInterval) {
if (window.confirm(t('statistics_retention_confirm'))) {
this.props.setStatsConfig(config);
}
} else {
this.props.setStatsConfig(config);
}
};
handleReset = () => {
const { t, resetStats } = this.props;
// eslint-disable-next-line no-alert
if (window.confirm(t('statistics_clear_confirm'))) {
resetStats();
}
};
render() {
const {
t,
interval,
customInterval,
processing,
processingReset,
ignored,
enabled,
} = this.props;
return (
<Card title={t('statistics_configuration')} bodyType="card-body box-body--settings" id="stats-config">
<div className="form">
<Form
initialValues={{
interval,
customInterval,
enabled,
ignored: ignored.join('\n'),
}}
processing={processing}
processingReset={processingReset}
onSubmit={this.handleFormSubmit}
onReset={this.handleReset}
/>
</div>
</Card>
);
}
}
export default withTranslation()(StatsConfig);

View File

@ -1,228 +0,0 @@
import React, { Component, Fragment } from 'react';
import { withTranslation } from 'react-i18next';
import i18next from 'i18next';
import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig';
import { FiltersConfig } from './FiltersConfig';
import { Checkbox } from '../ui/Controls/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';
import './Settings.css';
import { SettingsData } from '../../initialState';
const ORDER_KEY = 'order';
const SETTINGS = {
safebrowsing: {
enabled: false,
title: i18next.t('use_adguard_browsing_sec'),
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
testId: 'safebrowsing',
[ORDER_KEY]: 0,
},
parental: {
enabled: false,
title: i18next.t('use_adguard_parental'),
subtitle: i18next.t('use_adguard_parental_hint'),
testId: 'parental',
[ORDER_KEY]: 1,
},
};
interface SettingsProps {
initSettings: (...args: unknown[]) => unknown;
settings: SettingsData;
toggleSetting: (...args: unknown[]) => unknown;
getStatsConfig: (...args: unknown[]) => unknown;
setStatsConfig: (...args: unknown[]) => unknown;
resetStats: (...args: unknown[]) => unknown;
setFiltersConfig: (...args: unknown[]) => unknown;
getFilteringStatus: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
getLogsConfig?: (...args: unknown[]) => unknown;
setLogsConfig?: (...args: unknown[]) => unknown;
clearLogs?: (...args: unknown[]) => unknown;
stats?: {
processingGetConfig?: boolean;
interval?: number;
customInterval?: number;
enabled?: boolean;
ignored?: unknown[];
processingSetConfig?: boolean;
processingReset?: boolean;
};
queryLogs?: {
enabled?: boolean;
interval?: number;
customInterval?: number;
anonymize_client_ip?: boolean;
processingSetConfig?: boolean;
processingClear?: boolean;
processingGetConfig?: boolean;
ignored?: unknown[];
};
filtering?: {
interval?: number;
enabled?: boolean;
processingSetConfig?: boolean;
};
}
class Settings extends Component<SettingsProps> {
componentDidMount() {
this.props.initSettings(SETTINGS);
this.props.getStatsConfig();
this.props.getLogsConfig();
this.props.getFilteringStatus();
}
renderSettings = (settings: any) =>
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
const setting = settings[key];
const { enabled, title, subtitle, testId } = setting;
return (
<div key={key} className="form__group form__group--checkbox">
<Checkbox
data-testid={testId}
value={enabled}
title={title}
subtitle={subtitle}
onChange={(checked) => this.props.toggleSetting(key, !checked)}
/>
</div>
);
});
renderSafeSearch = () => {
const {
settings: {
settingsList: { safesearch },
},
} = this.props;
const { enabled } = safesearch || {};
const searches = { ...(safesearch || {}) };
delete searches.enabled;
return (
<>
<div className="form__group form__group--checkbox">
<Checkbox
data-testid="safesearch"
value={enabled}
title={i18next.t('enforce_safe_search')}
subtitle={i18next.t('enforce_save_search_hint')}
onChange={(checked) =>
this.props.toggleSetting('safesearch', { ...safesearch, enabled: checked })
}
/>
</div>
<div className="form__group--inner">
{Object.keys(searches).map((searchKey) => (
<div key={searchKey} className="form__group form__group--checkbox">
<Checkbox
value={searches[searchKey]}
title={captitalizeWords(searchKey)}
disabled={!safesearch.enabled}
onChange={(checked) =>
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
}
/>
</div>
))}
</div>
</>
);
};
render() {
const {
settings,
setStatsConfig,
resetStats,
stats,
queryLogs,
setLogsConfig,
clearLogs,
filtering,
setFiltersConfig,
t,
} = this.props;
const isDataReady = !settings.processing && !stats.processingGetConfig && !queryLogs.processingGetConfig;
return (
<Fragment>
<PageTitle title={t('general_settings')} />
{!isDataReady && <Loading />}
{isDataReady && (
<div className="content">
<div className="row">
<div className="col-md-12">
<Card bodyType="card-body box-body--settings">
<div className="form">
<FiltersConfig
initialValues={{
interval: filtering.interval,
enabled: filtering.enabled,
}}
processing={filtering.processingSetConfig}
setFiltersConfig={setFiltersConfig}
/>
{this.renderSettings(settings.settingsList)}
{this.renderSafeSearch()}
</div>
</Card>
</div>
<div className="col-md-12">
<LogsConfig
enabled={queryLogs.enabled}
ignored={queryLogs.ignored}
interval={queryLogs.interval}
customInterval={queryLogs.customInterval}
anonymize_client_ip={queryLogs.anonymize_client_ip}
processing={queryLogs.processingSetConfig}
processingClear={queryLogs.processingClear}
setLogsConfig={setLogsConfig}
clearLogs={clearLogs}
/>
</div>
<div className="col-md-12">
<StatsConfig
interval={stats.interval}
customInterval={stats.customInterval}
ignored={stats.ignored}
enabled={stats.enabled}
processing={stats.processingSetConfig}
processingReset={stats.processingReset}
setStatsConfig={setStatsConfig}
resetStats={resetStats}
/>
</div>
</div>
</div>
)}
</Fragment>
);
}
}
export default withTranslation()(Settings);

View File

@ -1,38 +0,0 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting } from '../actions';
import { getBlockedServices, updateBlockedServices } from '../actions/services';
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
import Settings from '../components/Settings';
const mapStateToProps = (state: any) => {
const { settings, services, stats, queryLogs, filtering } = state;
const props = {
settings,
services,
stats,
queryLogs,
filtering,
};
return props;
};
const mapDispatchToProps = {
initSettings,
toggleSetting,
getBlockedServices,
updateBlockedServices,
getStatsConfig,
setStatsConfig,
resetStats,
clearLogs,
getLogsConfig,
setLogsConfig,
getFilteringStatus,
setFiltersConfig,
};
export default connect(mapStateToProps, mapDispatchToProps)(Settings);