mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-10-26 11:27:18 +00:00
remove unused
This commit is contained in:
parent
66f5e215b4
commit
dbd0429d30
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
.form__button {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.form__button {
|
||||
margin-left: 0;
|
||||
margin-top: 1rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
Loading…
Reference in New Issue
Block a user