Pull request 743: + client: Query Logs Infinite Scroll
Merge in DNS/adguard-home from feature/infinite_scroll_query_logs to master
Squashed commit of the following:
commit 4407ef2e7c055066257da791fbd65e6b0a495729
Merge: 40b74522 0a4781be
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 16:20:23 2020 +0300
Merge branch 'master' into feature/infinite_scroll_query_logs
commit 40b745225112cf8d664220ed8f484b0aa16e997c
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 15:46:27 2020 +0300
Remove dynamic translation of toasts
commit f08fa7b8c6a243f6b10e924aebccc183ce7814fd
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 13:59:53 2020 +0300
Remove renderLimitIdx, update isEntireLog
commit 0f1b02616faaa5759c0a3f6d8257117fa22094d9
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:11:14 2020 +0300
Rename variables
commit 0928570c689c1fa704af775382620d68893e7c1c
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:06:50 2020 +0300
Make query logs short polling function more expressive
commit 9e773cbd6c287a1c799fa2680f3462508462ea7a
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:06:19 2020 +0300
Fix Toast translation interface
commit f9c57033e5adc5788954cf086b2f114dd8938bcb
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 17:01:36 2020 +0300
Do not hide loader
commit b86ba48613437f5559a748ad9aa4cf79d15db082
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 16:56:34 2020 +0300
Add dynamic translation for all toasts
commit b9d1d9b447ca90a3c179e503fa5d4abd3516321e
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 16:39:29 2020 +0300
Prevent getting query logs recursion if query is not changed
commit e25189749f7912648cca4503cfa8d0ad898c4bb6
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 10:13:20 2020 +0300
Decrease page limit to 20
commit 8b248ac5276899de838abf2dc9a69e47599cfc12
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 18:47:12 2020 +0300
Return checkFilteredLogs
commit bf2d65c4a3dca0da6b15f632ae11042b7c8e2045
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 18:33:51 2020 +0300
Review changes
commit 01b5250f9d9136a1f334086d3e2f00d1a928b37b
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 15:29:59 2020 +0300
Remove checkFilteredLogs
commit 25b364c41e6a1489d930c8b3b39b1ab43723f29d
Merge: 1dc66034 2c666cbd
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 14:28:47 2020 +0300
Merge branch 'feature/infinite_scroll_query_logs' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/infinite_scroll_query_logs
commit 1dc6603421cde9847e792bfe77ff6546e53fbc2a
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 14:28:01 2020 +0300
disregard maxFileScanEntries only if offset is set
commit bad741ed7f1dccf6959d43d000b8c0150f526f9e
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 11:57:45 2020 +0300
Fix search behavior when limit is specified
commit 2c666cbdde465cf17434126830dd99ceedfc4cbc
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 18:50:28 2020 +0300
Hide table ref loader during data loading
commit 8b4f7fe642ef9e87a979813dcdbd7817d64c27f9
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 18:43:24 2020 +0300
Repair search
commit 26fae1ae01a789999b8a2181d60b35663a20460a
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 17:59:27 2020 +0300
Resetting initial render index, change loader position on search
commit e2c97ae1a288438267eef9aec71b979319674a71
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 16:02:03 2020 +0300
Change isScrolledIntoView
... and 32 more commits
This commit is contained in:
109
client/src/components/Logs/Cells/ClientCell.js
Normal file
109
client/src/components/Logs/Cells/ClientCell.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import { checkFiltered } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
import IconTooltip from './IconTooltip';
|
||||
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||
|
||||
const ClientCell = ({
|
||||
client,
|
||||
domain,
|
||||
info,
|
||||
info: { name, whois_info },
|
||||
reason,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
country: whois_info?.country,
|
||||
city: whois_info?.city,
|
||||
network: whois_info?.orgname,
|
||||
source_label: source,
|
||||
};
|
||||
|
||||
const processedData = Object.entries(data);
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const buttonClass = classNames('btn btn-sm logs__cell--block-button', {
|
||||
'btn-outline-secondary': isFiltered,
|
||||
'btn-outline-danger': !isFiltered,
|
||||
});
|
||||
|
||||
const onClick = () => dispatch(toggleBlocking(buttonType, domain));
|
||||
|
||||
return <button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>;
|
||||
};
|
||||
|
||||
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 mw-75'
|
||||
xlinkHref='question' contentItemClass="contentItemClass" title="client_details"
|
||||
content={processedData} placement="bottom" />
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{renderFormattedClientCell(client, info, isDetailed, true)}
|
||||
</div>
|
||||
{isDetailed && name && !whoisAvailable
|
||||
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>
|
||||
{name}
|
||||
</div>}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
ClientCell.propTypes = {
|
||||
client: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
info: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.shape({
|
||||
name: propTypes.string.isRequired,
|
||||
whois_info: propTypes.shape({
|
||||
country: propTypes.string,
|
||||
city: propTypes.string,
|
||||
orgname: propTypes.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
reason: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ClientCell;
|
||||
29
client/src/components/Logs/Cells/DateCell.js
Normal file
29
client/src/components/Logs/Cells/DateCell.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import propTypes from 'prop-types';
|
||||
import { formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
|
||||
|
||||
const DateCell = ({ time }) => {
|
||||
const isDetailed = useSelector((state) => 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>;
|
||||
};
|
||||
|
||||
DateCell.propTypes = {
|
||||
time: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DateCell;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
@@ -9,15 +10,19 @@ import {
|
||||
} from '../../../helpers/constants';
|
||||
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
const getDomainCell = (props) => {
|
||||
const {
|
||||
row, t, isDetailed, dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
tracker, type, answer_dnssec, client_proto, domain, time,
|
||||
} = row.original;
|
||||
const DomainCell = ({
|
||||
answer_dnssec,
|
||||
client_proto,
|
||||
domain,
|
||||
time,
|
||||
tracker,
|
||||
type,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
@@ -50,8 +55,8 @@ const getDomainCell = (props) => {
|
||||
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>,
|
||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}</a>,
|
||||
};
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
@@ -72,51 +77,42 @@ const getDomainCell = (props) => {
|
||||
|
||||
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
||||
|
||||
const trackerHint = getIconTooltip({
|
||||
className: privacyIconClass,
|
||||
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
|
||||
xlinkHref: 'privacy',
|
||||
contentItemClass: 'key-colon',
|
||||
renderContent,
|
||||
place: 'bottom',
|
||||
});
|
||||
|
||||
const valueClass = classNames('w-100', {
|
||||
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="logs__row o-hidden">
|
||||
{dnssec_enabled && getIconTooltip({
|
||||
className: lockIconClass,
|
||||
tooltipClass: 'py-4 px-5 pb-45',
|
||||
canShowTooltip: answer_dnssec,
|
||||
xlinkHref: 'lock',
|
||||
columnClass: 'w-100',
|
||||
content: 'validated_with_dnssec',
|
||||
placement: 'bottom',
|
||||
})}
|
||||
{trackerHint}
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
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}
|
||||
place='bottom' />
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
};
|
||||
|
||||
getDomainCell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
toggleBlocking: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
dnssec_enabled: PropTypes.bool.isRequired,
|
||||
DomainCell.propTypes = {
|
||||
answer_dnssec: propTypes.bool.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
time: propTypes.string.isRequired,
|
||||
type: propTypes.string.isRequired,
|
||||
tracker: propTypes.object,
|
||||
};
|
||||
|
||||
export default getDomainCell;
|
||||
export default DomainCell;
|
||||
54
client/src/components/Logs/Cells/Header.js
Normal file
54
client/src/components/Logs/Cells/Header.js
Normal file
@@ -0,0 +1,54 @@
|
||||
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';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const isDetailed = useSelector((state) => 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;
|
||||
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const HeaderCell = ({ content, className }, idx) => {
|
||||
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>;
|
||||
};
|
||||
|
||||
HeaderCell.propTypes = {
|
||||
content: propTypes.oneOfType([propTypes.string, propTypes.element]).isRequired,
|
||||
className: propTypes.string,
|
||||
};
|
||||
|
||||
export default HeaderCell;
|
||||
@@ -7,7 +7,7 @@ import Tooltip from '../../ui/Tooltip';
|
||||
import 'react-popper-tooltip/dist/styles.css';
|
||||
import './IconTooltip.css';
|
||||
|
||||
const getIconTooltip = ({
|
||||
const IconTooltip = ({
|
||||
className,
|
||||
contentItemClass,
|
||||
columnClass,
|
||||
@@ -43,14 +43,14 @@ const getIconTooltip = ({
|
||||
</Tooltip>;
|
||||
};
|
||||
|
||||
getIconTooltip.propTypes = {
|
||||
IconTooltip.propTypes = {
|
||||
className: PropTypes.string,
|
||||
contentItemClass: PropTypes.string,
|
||||
columnClass: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
canShowTooltip: PropTypes.string,
|
||||
canShowTooltip: PropTypes.bool,
|
||||
xlinkHref: PropTypes.string,
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
@@ -59,4 +59,4 @@ getIconTooltip.propTypes = {
|
||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||
};
|
||||
|
||||
export default getIconTooltip;
|
||||
export default IconTooltip;
|
||||
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { formatElapsedMs, getFilterName } from '../../../helpers/helpers';
|
||||
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
const ResponseCell = ({
|
||||
elapsedMs,
|
||||
originalResponse,
|
||||
reason,
|
||||
response,
|
||||
status,
|
||||
upstream,
|
||||
rule,
|
||||
filterId,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
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 filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
const renderResponses = (responseArr) => {
|
||||
if (!responseArr || responseArr.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <div>{responseArr.map((response) => {
|
||||
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,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({
|
||||
...COMMON_CONTENT,
|
||||
filter: '',
|
||||
});
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
|
||||
return <div className="logs__cell logs__cell--response" role="gridcell">
|
||||
<IconTooltip
|
||||
className={classNames('icons mr-4 icon--24 icon--lightgray', { '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>;
|
||||
};
|
||||
|
||||
ResponseCell.propTypes = {
|
||||
elapsedMs: propTypes.string.isRequired,
|
||||
originalResponse: propTypes.array.isRequired,
|
||||
reason: propTypes.string.isRequired,
|
||||
response: propTypes.array.isRequired,
|
||||
status: propTypes.string.isRequired,
|
||||
upstream: propTypes.string.isRequired,
|
||||
rule: propTypes.string,
|
||||
filterId: propTypes.number,
|
||||
};
|
||||
|
||||
export default ResponseCell;
|
||||
@@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatClientCell } from '../../../helpers/formatClientCell';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
import { checkFiltered } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
|
||||
const getClientCell = ({
|
||||
row, t, isDetailed, toggleBlocking, autoClients, processingRules,
|
||||
}) => {
|
||||
const {
|
||||
reason, client, domain, info: { name, whois_info },
|
||||
} = row.original;
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
country: whois_info?.country,
|
||||
city: whois_info?.city,
|
||||
network: whois_info?.orgname,
|
||||
source_label: source,
|
||||
};
|
||||
|
||||
const processedData = Object.entries(data);
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const buttonClass = classNames('logs__action button__action', {
|
||||
'btn-outline-secondary': isFiltered,
|
||||
'btn-outline-danger': !isFiltered,
|
||||
'logs__action--detailed': isDetailed,
|
||||
});
|
||||
|
||||
const onClick = () => toggleBlocking(buttonType, domain);
|
||||
|
||||
return (
|
||||
<div className={buttonClass}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden h-100">
|
||||
{getIconTooltip({
|
||||
className: hintClass,
|
||||
columnClass: 'grid grid--limited',
|
||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75',
|
||||
xlinkHref: 'question',
|
||||
contentItemClass: 'text-truncate key-colon',
|
||||
title: 'client_details',
|
||||
content: processedData,
|
||||
placement: 'bottom',
|
||||
})}
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{formatClientCell(row, isDetailed)}
|
||||
</div>
|
||||
|
||||
{isDetailed && name && !whoisAvailable && (
|
||||
<div
|
||||
className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getClientCell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
toggleBlocking: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
processingRules: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default getClientCell;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { formatTime, formatDateTime } from '../../../helpers/helpers';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '../../../helpers/constants';
|
||||
|
||||
const getDateCell = (row, isDetailed) => {
|
||||
const { time } = row.original;
|
||||
|
||||
if (!time) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
|
||||
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
|
||||
|
||||
return (
|
||||
<div className="logs__cell">
|
||||
<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 getDateCell;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { formatElapsedMs } from '../../../helpers/helpers';
|
||||
import {
|
||||
FILTERED_STATUS,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
|
||||
const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
|
||||
const {
|
||||
reason, filterId, rule, status, upstream, elapsedMs, response, originalResponse,
|
||||
} = row.original;
|
||||
|
||||
const { filters, whitelistFilters } = filtering;
|
||||
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 filter = getFilterName(filters, whitelistFilters, filterId, t);
|
||||
|
||||
const renderResponses = (responseArr) => {
|
||||
if (responseArr?.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <div>{responseArr.map((response) => {
|
||||
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,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({ ...COMMON_CONTENT, filter: '' });
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{getIconTooltip({
|
||||
className: classNames('icons mr-4 icon--24 icon--lightgray', { '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,
|
||||
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 getResponseCell;
|
||||
197
client/src/components/Logs/Cells/index.js
Normal file
197
client/src/components/Logs/Cells/index.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import React, { memo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import propTypes from 'prop-types';
|
||||
import {
|
||||
captitalizeWords,
|
||||
checkFiltered,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
getFilterName,
|
||||
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 } from '../../../actions';
|
||||
import DateCell from './DateCell';
|
||||
import DomainCell from './DomainCell';
|
||||
import ResponseCell from './ResponseCell';
|
||||
import ClientCell from './ClientCell';
|
||||
import '../Logs.css';
|
||||
|
||||
const Row = memo(({
|
||||
style,
|
||||
rowProps,
|
||||
rowProps: { reason },
|
||||
isSmallScreen,
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
|
||||
const onClick = () => {
|
||||
if (!isSmallScreen) { return; }
|
||||
const {
|
||||
answer_dnssec,
|
||||
client,
|
||||
domain,
|
||||
elapsedMs,
|
||||
info,
|
||||
reason,
|
||||
response,
|
||||
time,
|
||||
tracker,
|
||||
upstream,
|
||||
type,
|
||||
client_proto,
|
||||
filterId,
|
||||
rule,
|
||||
originalResponse,
|
||||
status,
|
||||
} = rowProps;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const autoClient = autoClients
|
||||
.find((autoClient) => autoClient.name === client);
|
||||
|
||||
const { whois_info } = info;
|
||||
const country = whois_info?.country;
|
||||
const city = whois_info?.city;
|
||||
const network = whois_info?.orgname;
|
||||
|
||||
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 filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
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,
|
||||
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,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info?.name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
source_label: source,
|
||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||
original_response: originalResponse?.join('\n'),
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
setDetailedDataCurrent(processContent(detailedData));
|
||||
setButtonType(buttonType);
|
||||
setModalOpened(true);
|
||||
};
|
||||
|
||||
const isDetailed = useSelector((state) => 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">
|
||||
<DateCell {...rowProps} />
|
||||
<DomainCell {...rowProps} />
|
||||
<ResponseCell {...rowProps} />
|
||||
<ClientCell {...rowProps} />
|
||||
</div>;
|
||||
});
|
||||
|
||||
Row.displayName = 'Row';
|
||||
|
||||
Row.propTypes = {
|
||||
style: propTypes.object,
|
||||
rowProps: propTypes.shape({
|
||||
reason: propTypes.string.isRequired,
|
||||
answer_dnssec: propTypes.bool.isRequired,
|
||||
client: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
elapsedMs: propTypes.string.isRequired,
|
||||
info: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.shape({
|
||||
whois_info: propTypes.shape({
|
||||
country: propTypes.string,
|
||||
city: propTypes.string,
|
||||
orgname: propTypes.string,
|
||||
}),
|
||||
})]),
|
||||
response: propTypes.array.isRequired,
|
||||
time: propTypes.string.isRequired,
|
||||
tracker: propTypes.object,
|
||||
upstream: propTypes.string.isRequired,
|
||||
type: propTypes.string.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
filterId: propTypes.number,
|
||||
rule: propTypes.string,
|
||||
originalResponse: propTypes.array,
|
||||
status: propTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
isSmallScreen: propTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||
setButtonType: propTypes.func.isRequired,
|
||||
setModalOpened: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Row;
|
||||
@@ -107,7 +107,7 @@ const Form = (props) => {
|
||||
|
||||
const {
|
||||
response_status, search,
|
||||
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
} = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
|
||||
const [
|
||||
debouncedSearch,
|
||||
@@ -171,14 +171,14 @@ const Form = (props) => {
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
QUERY, LABEL, disabled,
|
||||
}) => (
|
||||
<option
|
||||
key={label}
|
||||
value={query}
|
||||
key={LABEL}
|
||||
value={QUERY}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(label)}
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
@@ -197,5 +197,4 @@ Form.propTypes = {
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
const Filters = ({ filter, setIsLoading }) => {
|
||||
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">
|
||||
@@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
refreshLogs: PropTypes.func.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
87
client/src/components/Logs/InfiniteTable.js
Normal file
87
client/src/components/Logs/InfiniteTable.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
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';
|
||||
|
||||
const InfiniteTable = ({
|
||||
isLoading,
|
||||
items,
|
||||
isSmallScreen,
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const loader = useRef(null);
|
||||
|
||||
const {
|
||||
isEntireLog,
|
||||
processingGetLogs,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
|
||||
const loading = isLoading || processingGetLogs;
|
||||
|
||||
const listener = useCallback(() => {
|
||||
if (loader.current && isScrolledIntoView(loader.current)) {
|
||||
dispatch(getLogs());
|
||||
}
|
||||
}, [loader.current, isScrolledIntoView, getLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
listener();
|
||||
}, [items.length < QUERY_LOGS_PAGE_LIMIT]);
|
||||
|
||||
useEffect(() => {
|
||||
const THROTTLE_TIME = 100;
|
||||
const throttledListener = throttle(listener, THROTTLE_TIME);
|
||||
|
||||
window.addEventListener('scroll', throttledListener);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderRow = (row, idx) => <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>;
|
||||
};
|
||||
|
||||
InfiniteTable.propTypes = {
|
||||
isLoading: propTypes.bool.isRequired,
|
||||
items: propTypes.array.isRequired,
|
||||
isSmallScreen: propTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||
setButtonType: propTypes.func.isRequired,
|
||||
setModalOpened: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default InfiniteTable;
|
||||
@@ -1,44 +1,21 @@
|
||||
: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-8: #888;
|
||||
--danger: #DF3812;
|
||||
--white80: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 26px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-table .logs__row {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs__row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--column {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--icons {
|
||||
max-width: 180px;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.logs__row .list-unstyled {
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__text,
|
||||
.logs__row .list-unstyled li {
|
||||
.logs__text {
|
||||
padding: 0 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -54,237 +31,6 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs__text--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs__text--wrap {
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.logs__text--nowrap {
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logs__text--whois {
|
||||
line-height: 1.2;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip__option {
|
||||
height: 2.5rem !important;
|
||||
width: 10.5rem;
|
||||
padding: 0.3125rem 1.5rem 0.6875rem;
|
||||
}
|
||||
|
||||
.tooltip__option:hover {
|
||||
background-color: var(--gray-f3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button__action {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.table__action {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.logs__action {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.logs__action--detailed {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.logs__table .rt-td,
|
||||
.clients__table .rt-td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead, .logs__table .rt-tbody {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr:hover .logs__action,
|
||||
.clients__table .rt-tr:hover .table__action {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:before {
|
||||
top: calc(100% + 12px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:after {
|
||||
top: initial;
|
||||
bottom: -4px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body {
|
||||
top: calc(100% + 5px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body:after {
|
||||
top: -11px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input,
|
||||
.logs__table .rt-thead.-filters select {
|
||||
padding: 6px 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters select {
|
||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background-size: 8px 10px;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input:focus,
|
||||
.logs__table .rt-thead.-filters select:focus {
|
||||
border-color: #1991eb;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
|
||||
.logs__text-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__list-wrap {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__list-item {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* New logs */
|
||||
.logs__table {
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 42rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__table--detailed {
|
||||
min-height: 50rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-header {
|
||||
box-shadow: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead .rt-th {
|
||||
padding: 0.9375rem 0.9375rem 0.875rem 0;
|
||||
text-align: left;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-td {
|
||||
padding: 1rem 1rem 0.5rem 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead .rt-th:last-child,
|
||||
.logs__table .rt-tbody .rt-td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-tr-group {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr {
|
||||
position: relative;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:not(:first-child) .rt-tr:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
right: 1.5rem;
|
||||
top: 0;
|
||||
width: calc(100% - 3rem);
|
||||
height: 2px;
|
||||
background-color: rgba(216, 216, 216, 0.23);
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:last-child .rt-tr:after,
|
||||
.logs__table .rt-thead .rt-tr:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs__time {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
@@ -302,132 +48,24 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Hide 3 and 4 column on mobile */
|
||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.text-pre {
|
||||
white-space: pre-wrap !important;
|
||||
overflow-wrap: break-word;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
width: 11.875rem !important;
|
||||
background-color: transparent;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.custom-pagination--padding {
|
||||
padding: 2.5rem 0 2.5rem !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn {
|
||||
--side-size: 2rem;
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--gray-d8) !important;
|
||||
border-radius: 4px !important;
|
||||
width: var(--side-size) !important;
|
||||
height: var(--side-size) !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn:enabled:hover {
|
||||
background-color: var(--gray-f3) !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-previous {
|
||||
flex: 0 1 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-next {
|
||||
flex: 0 1 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.logs__table .-pageInfo {
|
||||
--side-size: 2rem;
|
||||
font-variant-numeric: tabular-nums !important;
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--gray-d8) !important;
|
||||
border-radius: 4px !important;
|
||||
width: var(--side-size) !important;
|
||||
height: var(--side-size) !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logs__table .pagination-bottom {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.logs__table .-center:before {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.logs__table .-center:after {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.icon--detailed-info {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.link--green {
|
||||
color: var(--green79);
|
||||
}
|
||||
|
||||
.row--detailed {
|
||||
height: 4.9rem
|
||||
}
|
||||
|
||||
.w-90 {
|
||||
max-width: 90% !important;
|
||||
}
|
||||
|
||||
.h-85 {
|
||||
height: 85% !important;
|
||||
}
|
||||
|
||||
.pt-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
}
|
||||
|
||||
.pb-45 {
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.py-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.mh-100 {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
@@ -493,14 +131,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.rt-tr .logs__row .logs__text {
|
||||
max-width: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
.ml-small {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control--container {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
@@ -517,38 +147,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.logs__table .rt-tr {
|
||||
height: 3.125rem;
|
||||
@media screen and (max-width: 767.98px) {
|
||||
.logs__table .logs__cell--response,
|
||||
.logs__table .logs__cell--client {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-td {
|
||||
padding: 0.625rem 1rem 0.875rem 0;
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
min-height: 42rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading__container > .-loading-inner {
|
||||
top: 10rem !important;
|
||||
bottom: initial !important;
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
transform: translateY(3rem);
|
||||
}
|
||||
|
||||
.logs__refresh {
|
||||
--size: 2.5rem;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.logs__cell--block-button {
|
||||
max-height: 1.75rem;
|
||||
position: relative;
|
||||
left: 10%;
|
||||
top: 40%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 26px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs__table .logs__row {
|
||||
border-bottom: 2px solid var(--gray-216);
|
||||
}
|
||||
|
||||
.logs__table .logs__row:hover .logs__cell--block-button {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.logs__table .logs__row .logs__cell--block-button:disabled {
|
||||
background-color: var(--white) !important;
|
||||
}
|
||||
|
||||
/* QUERY_STATUS_COLORS */
|
||||
.logs__row--blue {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.logs__row--green {
|
||||
background-color: var(--green-pale);
|
||||
}
|
||||
|
||||
.logs__row--red {
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.logs__row--white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.logs__row--yellow {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.logs__no-data {
|
||||
color: var(--gray-4d);
|
||||
background-color: var(--white80);
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding-top: 21rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logs__loading {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
background-color: var(--white);
|
||||
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: scroll;
|
||||
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%;
|
||||
}
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
import classNames from 'classnames';
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
SCHEME_TO_PROTOCOL_MAP,
|
||||
CUSTOM_FILTERING_RULES_ID, FILTERED_STATUS,
|
||||
} from '../../helpers/constants';
|
||||
import getDateCell from './Cells/getDateCell';
|
||||
import getDomainCell from './Cells/getDomainCell';
|
||||
import getClientCell from './Cells/getClientCell';
|
||||
import getResponseCell from './Cells/getResponseCell';
|
||||
|
||||
import {
|
||||
captitalizeWords,
|
||||
checkFiltered,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
processContent,
|
||||
} from '../../helpers/helpers';
|
||||
import Loading from '../ui/Loading';
|
||||
import { getSourceData } from '../../helpers/trackers/trackers';
|
||||
|
||||
const Table = (props) => {
|
||||
const {
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
isSmallScreen,
|
||||
setIsLoading,
|
||||
filtering,
|
||||
isDetailed,
|
||||
toggleDetailedLogs,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
processingGetLogs,
|
||||
logs,
|
||||
pages,
|
||||
page,
|
||||
isLoading,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleBlocking = (type, domain) => {
|
||||
const {
|
||||
setRules, getFilteringStatus, addSuccessToast,
|
||||
} = props;
|
||||
const { userRules } = filtering;
|
||||
|
||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||
const baseRule = `||${domain}^$important`;
|
||||
const baseUnblocking = `@@${baseRule}`;
|
||||
|
||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
|
||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||
|
||||
if (matchPreparedBlockingRule) {
|
||||
setRules(userRules.replace(`${blockingRule}`, ''));
|
||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
} else if (!matchPreparedUnblockingRule) {
|
||||
setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
} else if (matchPreparedUnblockingRule) {
|
||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
return;
|
||||
} else if (!matchPreparedBlockingRule) {
|
||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
return;
|
||||
}
|
||||
|
||||
getFilteringStatus();
|
||||
};
|
||||
|
||||
const getFilterName = (filters, whitelistFilters, filterId, t) => {
|
||||
if (filterId === CUSTOM_FILTERING_RULES_ID) {
|
||||
return t('custom_filter_rules');
|
||||
}
|
||||
|
||||
const filter = filters.find((filter) => filter.id === filterId)
|
||||
|| whitelistFilters.find((filter) => filter.id === filterId);
|
||||
let filterName = '';
|
||||
|
||||
if (filter) {
|
||||
filterName = filter.name;
|
||||
}
|
||||
|
||||
if (!filterName) {
|
||||
filterName = t('unknown_filter', { filterId });
|
||||
}
|
||||
|
||||
return filterName;
|
||||
};
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
Cell: (row) => getDateCell(row, isDetailed),
|
||||
minWidth: 70,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: t('request_table_header'),
|
||||
accessor: 'domain',
|
||||
Cell: (row) => {
|
||||
const {
|
||||
isDetailed,
|
||||
autoClients,
|
||||
dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
return getDomainCell({
|
||||
row,
|
||||
t,
|
||||
isDetailed,
|
||||
toggleBlocking,
|
||||
autoClients,
|
||||
dnssec_enabled,
|
||||
});
|
||||
},
|
||||
minWidth: 180,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: t('response_table_header'),
|
||||
accessor: 'response',
|
||||
Cell: (row) => getResponseCell(
|
||||
row,
|
||||
filtering,
|
||||
t,
|
||||
isDetailed,
|
||||
getFilterName,
|
||||
),
|
||||
minWidth: 150,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: function Header() {
|
||||
return <div className="d-flex justify-content-between">
|
||||
{t('client_table_header')}
|
||||
{<span>
|
||||
<svg
|
||||
className={classNames('icons icon--24 icon--green mr-2 cursor--pointer', {
|
||||
'icon--selected': !isDetailed,
|
||||
})}
|
||||
onClick={() => toggleDetailedLogs(false)}
|
||||
>
|
||||
<title><Trans>compact</Trans></title>
|
||||
<use xlinkHref='#list' />
|
||||
</svg>
|
||||
<svg
|
||||
className={classNames('icons icon--24 icon--green cursor--pointer', {
|
||||
'icon--selected': isDetailed,
|
||||
})}
|
||||
onClick={() => toggleDetailedLogs(true)}
|
||||
>
|
||||
<title><Trans>default</Trans></title>
|
||||
<use xlinkHref='#detailed_list' />
|
||||
</svg>
|
||||
</span>}
|
||||
</div>;
|
||||
},
|
||||
accessor: 'client',
|
||||
Cell: (row) => {
|
||||
const {
|
||||
isDetailed,
|
||||
autoClients,
|
||||
filtering: { processingRules },
|
||||
} = props;
|
||||
|
||||
return getClientCell({
|
||||
row,
|
||||
t,
|
||||
isDetailed,
|
||||
toggleBlocking,
|
||||
autoClients,
|
||||
processingRules,
|
||||
});
|
||||
},
|
||||
minWidth: 123,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
className: 'pb-0',
|
||||
},
|
||||
];
|
||||
|
||||
const changePage = async (page) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const { oldest, getLogs, pages } = props;
|
||||
const isLastPage = pages && (page + 1 === pages);
|
||||
|
||||
await Promise.all([
|
||||
setLogsPage(page),
|
||||
setLogsPagination({
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
}),
|
||||
].concat(isLastPage ? getLogs(oldest, page) : []));
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const tableClass = classNames('logs__table', {
|
||||
'logs__table--detailed': isDetailed,
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactTable
|
||||
manual
|
||||
minRows={0}
|
||||
page={page}
|
||||
pages={pages}
|
||||
columns={columns}
|
||||
filterable={false}
|
||||
sortable={false}
|
||||
resizable={false}
|
||||
data={logs || []}
|
||||
loading={isLoading || processingGetLogs}
|
||||
showPageJump={false}
|
||||
showPageSizeOptions={false}
|
||||
onPageChange={changePage}
|
||||
className={tableClass}
|
||||
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
|
||||
loadingText={
|
||||
<>
|
||||
<Loading />
|
||||
<h6 className="loading__text">{t('loading_table_status')}</h6>
|
||||
</>
|
||||
}
|
||||
getLoadingProps={() => ({ className: 'loading__container' })}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={!processingGetLogs
|
||||
&& <label className="logs__text logs__text--bold">{t('nothing_found')}</label>}
|
||||
pageText=''
|
||||
ofText=''
|
||||
showPagination={logs.length > 0}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination custom-pagination--padding' })}
|
||||
getTbodyProps={() => ({ className: 'd-block' })}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
||||
<title><Trans>previous_btn</Trans></title>
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
||||
<title><Trans>next_btn</Trans></title>
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
renderTotalPagesCount={() => false}
|
||||
getTrGroupProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { reason } = rowInfo.original;
|
||||
const colorClass = FILTERED_STATUS_TO_META_MAP[reason] ? FILTERED_STATUS_TO_META_MAP[reason].color : 'white';
|
||||
|
||||
return { className: colorClass };
|
||||
}}
|
||||
getTrProps={(state, rowInfo) => ({
|
||||
className: isDetailed ? 'row--detailed' : '',
|
||||
onClick: () => {
|
||||
if (isSmallScreen) {
|
||||
const { dnssec_enabled, autoClients } = props;
|
||||
const {
|
||||
answer_dnssec,
|
||||
client,
|
||||
domain,
|
||||
elapsedMs,
|
||||
info,
|
||||
reason,
|
||||
response,
|
||||
time,
|
||||
tracker,
|
||||
upstream,
|
||||
type,
|
||||
client_proto,
|
||||
filterId,
|
||||
rule,
|
||||
originalResponse,
|
||||
status,
|
||||
} = rowInfo.original;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const autoClient = autoClients
|
||||
.find((autoClient) => autoClient.name === client);
|
||||
|
||||
const { whois_info } = info;
|
||||
const country = whois_info?.country;
|
||||
const city = whois_info?.city;
|
||||
const network = whois_info?.orgname;
|
||||
|
||||
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 = () => {
|
||||
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 { filters, whitelistFilters } = filtering;
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId, t);
|
||||
|
||||
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,
|
||||
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,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info?.name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
source_label: source,
|
||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||
original_response: originalResponse?.join('\n'),
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
setDetailedDataCurrent(processContent(detailedData));
|
||||
setButtonType(buttonType);
|
||||
setModalOpened(true);
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Table.propTypes = {
|
||||
logs: PropTypes.array.isRequired,
|
||||
pages: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
defaultPageSize: PropTypes.number,
|
||||
oldest: PropTypes.string.isRequired,
|
||||
filtering: PropTypes.object.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
processingGetConfig: PropTypes.bool.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
getLogs: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
dnssec_enabled: PropTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: PropTypes.func.isRequired,
|
||||
setButtonType: PropTypes.func.isRequired,
|
||||
setModalOpened: PropTypes.func.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
@@ -8,24 +7,21 @@ import queryString from 'query-string';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
SMALL_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
import Table from './Table';
|
||||
import Disabled from './Disabled';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import {
|
||||
getLogsConfig,
|
||||
refreshFilteredLogs,
|
||||
resetFilteredLogs,
|
||||
setFilteredLogs,
|
||||
toggleDetailedLogs,
|
||||
} from '../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../actions/toasts';
|
||||
import InfiniteTable from './InfiniteTable';
|
||||
import './Logs.css';
|
||||
|
||||
const processContent = (data, buttonType) => Object.entries(data)
|
||||
@@ -48,21 +44,20 @@ const processContent = (data, buttonType) => Object.entries(data)
|
||||
keyClass = '';
|
||||
}
|
||||
|
||||
return isHidden ? null : <Fragment key={key}>
|
||||
return isHidden ? null : <div key={key}>
|
||||
<div
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}>
|
||||
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>
|
||||
</Fragment>;
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
||||
const Logs = (props) => {
|
||||
const Logs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
@@ -71,7 +66,14 @@ const Logs = (props) => {
|
||||
search: search_url_param = '',
|
||||
} = queryString.parse(history.location.search);
|
||||
|
||||
const { filter } = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
|
||||
const logs = useSelector((state) => state.queryLogs.logs, shallowEqual);
|
||||
|
||||
const search = filter?.search || search_url_param;
|
||||
const response_status = filter?.response_status || response_status_url_param;
|
||||
@@ -82,6 +84,7 @@ const Logs = (props) => {
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -94,44 +97,11 @@ const Logs = (props) => {
|
||||
})();
|
||||
}, [response_status, search]);
|
||||
|
||||
const {
|
||||
filtering,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
toggleDetailedLogs,
|
||||
dashboard,
|
||||
dnsConfig,
|
||||
queryLogs: {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
oldest,
|
||||
logs,
|
||||
pages,
|
||||
page,
|
||||
isDetailed,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
toggleDetailedLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
const getLogs = (older_than, page, initial) => {
|
||||
if (enabled) {
|
||||
props.getLogs({
|
||||
older_than,
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
initial,
|
||||
});
|
||||
dispatch(toggleDetailedLogs(false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,7 +119,6 @@ const Logs = (props) => {
|
||||
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(getClients());
|
||||
try {
|
||||
@@ -169,6 +138,7 @@ const Logs = (props) => {
|
||||
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);
|
||||
@@ -179,99 +149,53 @@ const Logs = (props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await Promise.all([
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||
dispatch(refreshFilteredLogs()),
|
||||
]);
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
};
|
||||
const renderPage = () => <>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
/>
|
||||
<InfiniteTable
|
||||
isLoading={isLoading}
|
||||
items={logs}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>
|
||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: 0,
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
refreshLogs={refreshLogs}
|
||||
/>
|
||||
<Table
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
logs={logs}
|
||||
pages={pages}
|
||||
page={page}
|
||||
autoClients={dashboard.autoClients}
|
||||
oldest={oldest}
|
||||
filtering={filtering}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingGetConfig={processingGetConfig}
|
||||
isDetailed={isDetailed}
|
||||
setLogsPagination={setLogsPagination}
|
||||
setLogsPage={setLogsPage}
|
||||
toggleDetailedLogs={toggleDetailedLogs}
|
||||
getLogs={getLogs}
|
||||
setRules={props.setRules}
|
||||
addSuccessToast={props.addSuccessToast}
|
||||
getFilteringStatus={props.getFilteringStatus}
|
||||
dnssec_enabled={dnsConfig.dnssec_enabled}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: 0,
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<Disabled />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Logs.propTypes = {
|
||||
getLogs: PropTypes.func.isRequired,
|
||||
queryLogs: PropTypes.object.isRequired,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
filtering: PropTypes.object.isRequired,
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
return <>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && renderPage()}
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
|
||||
Reference in New Issue
Block a user