Merge: fix #1421
Full rework of the query log Squashed commit of the following: commit e8a72eb223551f17e637136713dae03accf8ab9e Author: Andrey Meshkov <am@adguard.com> Date: Thu Jun 18 00:31:53 2020 +0300 fix race in whois test commit 801d28197f888fa21f83c9a0b49e3c9472c08513 Merge: 9d9787fdb1c951fbAuthor: Andrey Meshkov <am@adguard.com> Date: Thu Jun 18 00:28:13 2020 +0300 Merge branch 'master' into feature/1421 commit 9d9787fd79b17f76c7baed52c12ac462fd00a5e4 Merge: 4ce337ca 08e238ab Author: Andrey Meshkov <am@adguard.com> Date: Thu Jun 18 00:27:32 2020 +0300 Merge commit 4ce337ca7aec163edf87a038bb25fb44e64f8613 Author: Andrey Meshkov <am@adguard.com> Date: Thu Jun 18 00:22:49 2020 +0300 -(home): fix whois test commit 08e238ab0e723b1e354f58245e9a8d5017b392c9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jun 18 00:13:41 2020 +0300 Add comments commit 5f108065952bcc25dce1c2eee3f9401d2641a6e9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 23:47:50 2020 +0300 Make tooltip position absolute for touch commit 4c30a583165e5d007d4b01b657de8751a7bd8c7b Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 20:39:44 2020 +0300 Prevent scroll hide for touch devices commit 62da97931f5921613762614717c62c77ddb6b8db Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 20:06:24 2020 +0300 Review changes: ipad tooltip commit 12dddcca8caca51c157b5d25dfa3ca03ba7f0c06 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 16:59:16 2020 +0300 Add close tooltip event for ipad commit 62191e41d5bf67317f9f1dc6c6af08cbabb4bf90 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 16:39:40 2020 +0300 Add success toast on logs refresh commit 2ebdd6a8124269d737c8060c3247aaf35d85cb8b Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 16:01:37 2020 +0300 Fix pagination commit 5820c92bacd93d05a3d66d42ee95f099e1c5d9e9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jun 17 11:31:15 2020 +0300 Revert "Render table in chunks" This reverts commit cdfcd849ccddc1bc35591edac7904129431470c9. commit cdfcd849ccddc1bc35591edac7904129431470c9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 18:42:18 2020 +0300 Render table in chunks commit cc8c5e64274bf6e806e2e8a4bf305af745c3ed2a Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 17:35:24 2020 +0300 Add pagination button hover effect commit f7e134091a1556784a5fea9d83c50353536126ef Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 16:28:00 2020 +0300 Make loader position absolute commit a7b887b57d903f1f7ac967b861b5cc677728efc4 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 15:42:20 2020 +0300 Ignore clients find without params commit ecb322fefd4a161d79f28d17fe27827ee91701e4 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 15:30:48 2020 +0300 Styles changes commit 9323ce3938bf04e1290eade09201ba0790a250c0 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 14:32:23 2020 +0300 Review styles changes commit e0faa04ba3643f01b2ca99524cdd52b0731725c7 Merge: 9857682315e71435Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jun 16 12:08:45 2020 +0300 Merge branch '1421-new-qlog-v2' into feature/1421 commit 9857682371e8d9a3a91933cfb58a26b3470675d9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jun 15 18:32:02 2020 +0300 Fix response cell ... and 88 more commits
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
:root {
|
||||
--yellow-pale: rgba(247, 181, 0, 0.1);
|
||||
--green79: #67B279;
|
||||
--gray-a5: #a5a5a5;
|
||||
--gray-d8: #d8d8d8;
|
||||
--gray-f3: #F3F3F3;
|
||||
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
||||
}
|
||||
|
||||
@@ -22,6 +27,16 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container--wrap {
|
||||
min-height: calc(100vh);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -31,10 +46,17 @@ body {
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
@media (max-width: 575px) {
|
||||
.container {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
.mw-75 {
|
||||
max-width: 75% !important;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class App extends Component {
|
||||
)}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap">
|
||||
<div className="container container--wrap pb-5">
|
||||
{dashboard.processing && <Loading />}
|
||||
{!dashboard.isCoreRunning && (
|
||||
<div className="row row-cards">
|
||||
|
||||
@@ -32,17 +32,17 @@ const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) =>
|
||||
const buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND
|
||||
? {
|
||||
className: 'btn-outline-danger',
|
||||
text: 'block_btn',
|
||||
text: 'block',
|
||||
type: 'block',
|
||||
}
|
||||
: {
|
||||
className: 'btn-outline-secondary',
|
||||
text: 'unblock_btn',
|
||||
text: 'unblock',
|
||||
type: 'unblock',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table__action">
|
||||
<div className="table__action button__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonProps.className}`}
|
||||
|
||||
@@ -10,7 +10,7 @@ import BlockedDomains from './BlockedDomains';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Loading from '../ui/Loading';
|
||||
import { ACTION } from '../../helpers/constants';
|
||||
import { BLOCK_ACTIONS } from '../../helpers/constants';
|
||||
import './Dashboard.css';
|
||||
|
||||
class Dashboard extends Component {
|
||||
@@ -42,7 +42,7 @@ class Dashboard extends Component {
|
||||
};
|
||||
|
||||
toggleClientStatus = (type, ip) => {
|
||||
const confirmMessage = type === ACTION.block ? 'client_confirm_block' : 'client_confirm_unblock';
|
||||
const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock';
|
||||
|
||||
if (window.confirm(this.props.t(confirmMessage, { ip }))) {
|
||||
this.props.toggleClientBlock(type, ip);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { withTranslation } from 'react-i18next';
|
||||
|
||||
class Table extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
@@ -31,7 +31,7 @@ class Table extends Component {
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
className="btn btn-icon btn-icon--green btn-outline-secondary btn-sm"
|
||||
onClick={() => this.props.handleDelete({
|
||||
answer: value.row.answer,
|
||||
domain: value.row.domain,
|
||||
@@ -59,16 +59,26 @@ class Table extends Component {
|
||||
columns={this.columns}
|
||||
loading={processing || processingAdd || processingDelete}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
previousText={
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText="/"
|
||||
pageText=''
|
||||
ofText=''
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('rewrite_not_found')}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class Table extends Component {
|
||||
accessor: 'url',
|
||||
minWidth: 200,
|
||||
Cell: ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
{isValidAbsolutePath(value) ? value
|
||||
: <a
|
||||
href={value}
|
||||
@@ -126,17 +126,26 @@ class Table extends Component {
|
||||
<ReactTable
|
||||
data={filters}
|
||||
columns={this.columns}
|
||||
showPagination={true}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
loading={loading}
|
||||
minRows={6}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText="/"
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={whitelist ? t('no_whitelist_added') : t('no_blocklist_added')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
previousText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
106
client/src/components/Logs/Cells/getClientCell.js
Normal file
106
client/src/components/Logs/Cells/getClientCell.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatClientCell } from '../../../helpers/formatClientCell';
|
||||
import getHintElement from './getHintElement';
|
||||
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 },
|
||||
} = row.original;
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const country = autoClient && autoClient.whois_info && autoClient.whois_info.country;
|
||||
const city = autoClient && autoClient.whois_info && autoClient.whois_info.city;
|
||||
const network = autoClient && autoClient.whois_info && autoClient.whois_info.orgname;
|
||||
const source = autoClient && autoClient.source;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
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,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--small cursor--pointer icon--light-gray', {
|
||||
'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">
|
||||
{processedData && getHintElement({
|
||||
className: hintClass,
|
||||
columnClass: 'grid grid--limited',
|
||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75',
|
||||
dataTip: true,
|
||||
xlinkHref: 'question',
|
||||
contentItemClass: 'text-truncate key-colon',
|
||||
title: 'client_details',
|
||||
content: processedData,
|
||||
place: 'bottom',
|
||||
})}
|
||||
<div
|
||||
className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>{formatClientCell(row, t, isDetailed)}</div>
|
||||
{isDetailed && name
|
||||
&& <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;
|
||||
28
client/src/components/Logs/Cells/getDateCell.js
Normal file
28
client/src/components/Logs/Cells/getDateCell.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
121
client/src/components/Logs/Cells/getDomainCell.js
Normal file
121
client/src/components/Logs/Cells/getDomainCell.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import getHintElement from './getHintElement';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
SCHEME_TO_PROTOCOL_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
import { formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
|
||||
const getDomainCell = (props) => {
|
||||
const {
|
||||
row, t, isDetailed, dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
tracker, type, answer_dnssec, client_proto, domain, time,
|
||||
} = row.original;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const source = tracker && tracker.sourceData && tracker.sourceData.name;
|
||||
|
||||
const lockIconClass = classNames('icons', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', {
|
||||
'icon--active': answer_dnssec,
|
||||
'icon--disabled': !answer_dnssec,
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const privacyIconClass = classNames('icons', 'mx-2', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', {
|
||||
'icon--active': hasTracker,
|
||||
'icon--disabled': !hasTracker,
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const dnssecHint = getHintElement({
|
||||
className: lockIconClass,
|
||||
tooltipClass: 'py-4 px-5 pb-45',
|
||||
dataTip: answer_dnssec,
|
||||
xlinkHref: 'lock',
|
||||
columnClass: 'w-100',
|
||||
content: 'validated_with_dnssec',
|
||||
place: 'bottom',
|
||||
});
|
||||
|
||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||
const ip = type ? `${t('type_table_header')}: ${type}` : '';
|
||||
|
||||
const requestDetailsObj = {
|
||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
domain,
|
||||
type_table_header: type,
|
||||
protocol,
|
||||
};
|
||||
|
||||
const knownTrackerDataObj = {
|
||||
name_table_header: tracker && tracker.name,
|
||||
category_label: tracker && tracker.category,
|
||||
source_label: source && <a href={`//${source}`} className="link--green">{source}</a>,
|
||||
};
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
const preparedContent = typeof content === 'string' ? t(content) : content;
|
||||
const className = classNames('text-truncate key-colon o-hidden', {
|
||||
'word-break--break-all white-space--normal': preparedContent.length > 100,
|
||||
});
|
||||
return <div key={idx} className={className}>{preparedContent}</div>;
|
||||
};
|
||||
|
||||
const getGrid = (contentObj, title, className) => [
|
||||
<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 trackerHint = getHintElement({
|
||||
className: privacyIconClass,
|
||||
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
|
||||
dataTip: true,
|
||||
xlinkHref: 'privacy',
|
||||
contentItemClass: 'key-colon',
|
||||
renderContent,
|
||||
place: 'bottom',
|
||||
});
|
||||
|
||||
const valueClass = classNames('w-100', {
|
||||
'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 && dnssecHint}
|
||||
{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>
|
||||
</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,
|
||||
};
|
||||
|
||||
export default getDomainCell;
|
||||
76
client/src/components/Logs/Cells/getHintElement.js
Normal file
76
client/src/components/Logs/Cells/getHintElement.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomTooltip from '../Tooltip/CustomTooltip';
|
||||
|
||||
const getHintElement = ({
|
||||
className,
|
||||
contentItemClass,
|
||||
columnClass,
|
||||
dataTip,
|
||||
xlinkHref,
|
||||
content,
|
||||
title,
|
||||
place,
|
||||
tooltipClass,
|
||||
trigger,
|
||||
overridePosition,
|
||||
scrollHide,
|
||||
renderContent,
|
||||
}) => {
|
||||
const id = 'id';
|
||||
|
||||
const [isHovered, hover] = useState(false);
|
||||
|
||||
const openTooltip = () => hover(true);
|
||||
const closeTooltip = () => hover(false);
|
||||
|
||||
return <div onMouseEnter={openTooltip}
|
||||
onMouseLeave={closeTooltip}>
|
||||
<div data-tip={dataTip}
|
||||
data-for={dataTip ? id : undefined}
|
||||
data-event={trigger}
|
||||
>
|
||||
{xlinkHref && <svg className={className}>
|
||||
<use xlinkHref={`#${xlinkHref}`} />
|
||||
</svg>}
|
||||
</div>
|
||||
{isHovered && dataTip
|
||||
&& <CustomTooltip
|
||||
className={tooltipClass}
|
||||
id={id}
|
||||
columnClass={columnClass}
|
||||
contentItemClass={contentItemClass}
|
||||
title={title}
|
||||
place={place}
|
||||
content={content}
|
||||
trigger={trigger}
|
||||
overridePosition={overridePosition}
|
||||
scrollHide={scrollHide}
|
||||
renderContent={renderContent}
|
||||
/>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
getHintElement.propTypes = {
|
||||
className: PropTypes.string,
|
||||
contentItemClass: PropTypes.string,
|
||||
columnClass: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
place: PropTypes.string,
|
||||
dataTip: PropTypes.string,
|
||||
xlinkHref: PropTypes.string,
|
||||
overridePosition: PropTypes.func,
|
||||
scrollHide: PropTypes.bool,
|
||||
trigger: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.array,
|
||||
]),
|
||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||
};
|
||||
|
||||
export default getHintElement;
|
||||
120
client/src/components/Logs/Cells/getResponseCell.js
Normal file
120
client/src/components/Logs/Cells/getResponseCell.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { formatElapsedMs } from '../../../helpers/helpers';
|
||||
import {
|
||||
CUSTOM_FILTERING_RULES_ID,
|
||||
FILTERED_STATUS,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
import getHintElement from './getHintElement';
|
||||
|
||||
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 getResponseCell = (row, filtering, t, isDetailed) => {
|
||||
const {
|
||||
reason, filterId, rule, status, upstream, elapsedMs, domain, response,
|
||||
} = row.original;
|
||||
|
||||
const { filters, whitelistFilters } = filtering;
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
|
||||
const statusLabel = t((FILTERED_STATUS_TO_META_MAP[reason]
|
||||
&& 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', {
|
||||
'white-space--normal': response.length > 100,
|
||||
});
|
||||
|
||||
return <div key={response} className={className}>{`${response}\n`}</div>;
|
||||
})}</div>;
|
||||
};
|
||||
|
||||
const FILTERED_STATUS_TO_FIELDS_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||
domain,
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
response_table_header: renderResponses(response),
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||
domain,
|
||||
encryption_status: boldStatusLabel,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||
domain,
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||
domain,
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
},
|
||||
};
|
||||
|
||||
const fields = FILTERED_STATUS_TO_FIELDS_MAP[reason]
|
||||
? Object.entries(FILTERED_STATUS_TO_FIELDS_MAP[reason])
|
||||
: Object.entries(FILTERED_STATUS_TO_FIELDS_MAP.NotFilteredNotFound);
|
||||
|
||||
const detailedInfo = reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE
|
||||
|| reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||
? filter : formattedElapsedMs;
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{fields && getHintElement({
|
||||
className: classNames('icons mr-4 icon--small cursor--pointer icon--light-gray', { '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',
|
||||
dataTip: true,
|
||||
xlinkHref: 'question',
|
||||
title: 'response_details',
|
||||
content: fields,
|
||||
place: '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;
|
||||
30
client/src/components/Logs/Disabled.js
Normal file
30
client/src/components/Logs/Disabled.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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,11 +1,9 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderInputField } from '../../../helpers/form';
|
||||
import { FORM_NAME, RESPONSE_FILTER } from '../../../helpers/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { DEBOUNCE_FILTER_TIMEOUT, FORM_NAME, RESPONSE_FILTER } from '../../../helpers/constants';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
|
||||
const renderFilterField = ({
|
||||
@@ -18,25 +16,28 @@ const renderFilterField = ({
|
||||
autoComplete,
|
||||
tooltip,
|
||||
meta: { touched, error },
|
||||
}) => <Fragment>
|
||||
<div className="logs__input-wrap">
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
<span className="logs__notice">
|
||||
}) => <>
|
||||
<div className="input-group-search">
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder} />
|
||||
<span className="logs__notice">
|
||||
<Tooltip text={tooltip} type='tooltip-custom--logs' />
|
||||
</span>
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</div>
|
||||
</Fragment>;
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
|
||||
renderFilterField.propTypes = {
|
||||
input: PropTypes.object.isRequired,
|
||||
@@ -55,62 +56,47 @@ renderFilterField.propTypes = {
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleChange,
|
||||
className = '',
|
||||
responseStatusClass,
|
||||
submit,
|
||||
} = props;
|
||||
|
||||
const [t] = useTranslation();
|
||||
|
||||
const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT);
|
||||
const zeroDelaySubmit = () => setTimeout(submit, 0);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleChange}>
|
||||
<div className="row">
|
||||
<div className="col-6 col-sm-3 my-2">
|
||||
<Field
|
||||
id="filter_domain"
|
||||
name="filter_domain"
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('domain_name_table_header')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-sm-3 my-2">
|
||||
<Field
|
||||
id="filter_question_type"
|
||||
name="filter_question_type"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('type_table_header')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-sm-3 my-2">
|
||||
<Field
|
||||
name="filter_response_status"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value={RESPONSE_FILTER.ALL}>
|
||||
{t('show_all_filter_type')}
|
||||
</option>
|
||||
<option value={RESPONSE_FILTER.FILTERED}>
|
||||
{t('show_filtered_type')}
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="col-6 col-sm-3 my-2">
|
||||
<Field
|
||||
id="filter_client"
|
||||
name="filter_client"
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('client_table_header')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<form className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
zeroDelaySubmit();
|
||||
debouncedSubmit.cancel();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
id="search"
|
||||
name="search"
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={`form-control--search form-control--transparent ${className}`}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onChange={debouncedSubmit}
|
||||
/>
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name="response_status"
|
||||
component="select"
|
||||
className={`form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent ${responseStatusClass}`}
|
||||
onChange={zeroDelaySubmit}
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
}) => <option key={label} value={query}
|
||||
disabled={disabled}>{t(label)}</option>)}
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -118,12 +104,11 @@ const Form = (props) => {
|
||||
|
||||
Form.propTypes = {
|
||||
handleChange: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
responseStatusClass: PropTypes.string,
|
||||
submit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
}),
|
||||
])(Form);
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
})(Form);
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { DEBOUNCE_FILTER_TIMEOUT, RESPONSE_FILTER } from '../../../helpers/constants';
|
||||
import { isValidQuestionType } from '../../../helpers/helpers';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
|
||||
class Filters extends Component {
|
||||
getFilters = ({
|
||||
filter_domain, filter_question_type, filter_response_status, filter_client,
|
||||
}) => ({
|
||||
filter_domain: filter_domain || '',
|
||||
filter_question_type: isValidQuestionType(filter_question_type) ? filter_question_type.toUpperCase() : '',
|
||||
filter_response_status: filter_response_status === RESPONSE_FILTER.FILTERED ? filter_response_status : '',
|
||||
filter_client: filter_client || '',
|
||||
});
|
||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
handleFormChange = debounce((values) => {
|
||||
const filter = this.getFilters(values);
|
||||
this.props.setLogsFilter(filter);
|
||||
}, DEBOUNCE_FILTER_TIMEOUT);
|
||||
const onSubmit = async (values) => {
|
||||
setIsLoading(true);
|
||||
await dispatch(setLogsFilter(values));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { filter, processingAdditionalLogs } = this.props;
|
||||
return (
|
||||
<div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
<Trans>query_log</Trans>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon--green ml-3 bg-transparent"
|
||||
onClick={refreshLogs}
|
||||
>
|
||||
<svg className="icons icon--small">
|
||||
<use xlinkHref="#update" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
const cardBodyClass = classnames({
|
||||
'card-body': true,
|
||||
'card-body--loading': processingAdditionalLogs,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card bodyType={cardBodyClass}>
|
||||
<Form
|
||||
initialValues={filter}
|
||||
onChange={this.handleFormChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
</h1>
|
||||
<Form
|
||||
responseStatusClass="d-sm-block"
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
setLogsFilter: PropTypes.func.isRequired,
|
||||
refreshLogs: PropTypes.func.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
processingAdditionalLogs: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
:root {
|
||||
--gray-4d: #4D4D4D;
|
||||
--gray-8: #888;
|
||||
--danger: #DF3812;
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs__row--center {
|
||||
@@ -15,10 +22,6 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__row--icons {
|
||||
max-width: 180px;
|
||||
flex-flow: row wrap;
|
||||
@@ -35,6 +38,15 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
color: var(--gray-4d);
|
||||
letter-spacing: 0;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.logs__text--bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs__text--full {
|
||||
@@ -51,6 +63,11 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.logs__text--nowrap {
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logs__text--whois {
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -61,11 +78,18 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.logs__action,
|
||||
.table__action {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 15px;
|
||||
.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;
|
||||
@@ -73,11 +97,31 @@
|
||||
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;
|
||||
@@ -152,14 +196,14 @@
|
||||
}
|
||||
|
||||
.logs__notice {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
top: 0.5rem;
|
||||
right: 2rem;
|
||||
margin-top: 0.1875rem;
|
||||
font-size: 0.75rem;
|
||||
text-align: left;
|
||||
color: #a5a5a5;
|
||||
color: var(--gray-a5);
|
||||
}
|
||||
|
||||
.logs__whois {
|
||||
@@ -185,3 +229,348 @@
|
||||
margin-right: 1px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* New logs */
|
||||
.logs__table {
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 42rem;
|
||||
max-width: 71rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.detailed-info {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.icon--selected {
|
||||
background-color: var(--gray-f3);
|
||||
border: solid 1px var(--gray-d8);
|
||||
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;
|
||||
}
|
||||
|
||||
.-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;
|
||||
}
|
||||
|
||||
.pagination-bottom {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.-center:before {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.-center:after {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.icon--detailed-info {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.icon--light-gray {
|
||||
color: var(--gray-8);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cursor--pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-select__arrow--left {
|
||||
background: #fff url('./chevron-down.svg') no-repeat left 0.2rem center;
|
||||
background-size: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-select--logs {
|
||||
padding: 0.5rem 0.75rem 0.5rem 1.75rem !important;
|
||||
}
|
||||
|
||||
.bg--danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.ml-small {
|
||||
margin-left: 3.3125rem;
|
||||
}
|
||||
|
||||
.form-control--search {
|
||||
width: 39.125rem;
|
||||
box-shadow: 0 1px 0 #ddd;
|
||||
padding: 0 2.5rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.form-control--transparent {
|
||||
border: 0 solid transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.input-group-search {
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
left: 2rem;
|
||||
top: 0.4rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control--container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1279.98px) {
|
||||
.form-control--search {
|
||||
max-width: 30.125rem;
|
||||
}
|
||||
|
||||
.form-control--container {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.form-control--search {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.form-control--search {
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.form-control--container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.rt-tr .logs__row .logs__text {
|
||||
max-width: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
.ml-small {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.logs__table .rt-tr {
|
||||
height: 3.125rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-td {
|
||||
padding: 0.625rem 1rem 0.875rem 0;
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
min-height: 42rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.form-control--search {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.field__select {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading__container > .-loading-inner {
|
||||
top: 10rem !important;
|
||||
bottom: initial !important;
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
transform: translateY(3rem);
|
||||
}
|
||||
|
||||
/*reset position to make absolute position of tooltip on tablets, may cause problems https://github.com/wwayne/react-tooltip/issues/204*/
|
||||
@media (hover: none) {
|
||||
.logs__action {
|
||||
top: 1rem !important;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-td,
|
||||
.clients__table .rt-td {
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
|
||||
399
client/src/components/Logs/Table.js
Normal file
399
client/src/components/Logs/Table.js
Normal file
@@ -0,0 +1,399 @@
|
||||
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,
|
||||
} from '../../helpers/constants';
|
||||
import getDateCell from './Cells/getDateCell';
|
||||
import getDomainCell from './Cells/getDomainCell';
|
||||
import getClientCell from './Cells/getClientCell';
|
||||
import getResponseCell from './Cells/getResponseCell';
|
||||
|
||||
import {
|
||||
checkFiltered,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
|
||||
} from '../../helpers/helpers';
|
||||
import Loading from '../ui/Loading';
|
||||
|
||||
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 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,
|
||||
),
|
||||
minWidth: 150,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: () => {
|
||||
const plainSelected = classNames('cursor--pointer', {
|
||||
'icon--selected': !isDetailed,
|
||||
});
|
||||
|
||||
const detailedSelected = classNames('cursor--pointer', {
|
||||
'icon--selected': isDetailed,
|
||||
});
|
||||
|
||||
return <div className="d-flex justify-content-between">
|
||||
{t('client_table_header')}
|
||||
{<span>
|
||||
<svg
|
||||
className={`icons icon--small icon--active mr-2 cursor--pointer ${plainSelected}`}
|
||||
onClick={() => toggleDetailedLogs(false)}
|
||||
>
|
||||
<title><Trans>compact</Trans></title>
|
||||
<use xlinkHref='#list' />
|
||||
</svg>
|
||||
<svg
|
||||
className={`icons icon--small icon--active cursor--pointer ${detailedSelected}`}
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
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}
|
||||
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--small icon--gray w-100 h-100 cursor--pointer">
|
||||
<title><Trans>previous_btn</Trans></title>
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--small 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,
|
||||
} = rowInfo.original;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const autoClient = autoClients.find(
|
||||
(autoClient) => autoClient.name === client,
|
||||
);
|
||||
|
||||
const country = autoClient && autoClient.whois_info
|
||||
&& autoClient.whois_info.country;
|
||||
|
||||
const network = autoClient && autoClient.whois_info
|
||||
&& autoClient.whois_info.orgname;
|
||||
|
||||
const city = autoClient && autoClient.whois_info
|
||||
&& autoClient.whois_info.city;
|
||||
|
||||
const source = autoClient && autoClient.source;
|
||||
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
const onToggleBlock = () => {
|
||||
toggleBlocking(buttonType, domain);
|
||||
};
|
||||
|
||||
const tracker_source = tracker && tracker.sourceData
|
||||
&& tracker.sourceData.name;
|
||||
|
||||
const status = t((FILTERED_STATUS_TO_META_MAP[reason]
|
||||
&& FILTERED_STATUS_TO_META_MAP[reason].label) || reason);
|
||||
const statusBlocked = <div className="bg--danger">{status}</div>;
|
||||
|
||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||
|
||||
const detailedData = {
|
||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
encryption_status: status,
|
||||
domain,
|
||||
type_table_header: type,
|
||||
protocol,
|
||||
known_tracker: hasTracker && 'title',
|
||||
table_name: hasTracker && tracker.name,
|
||||
category_label: hasTracker && tracker.category,
|
||||
tracker_source: hasTracker && tracker_source && <a href={`//${source}`}
|
||||
className="link--green">{tracker_source}</a>,
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_table_header: response && response.join('\n'),
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info && info.name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
source_label: source,
|
||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className="title--border bg--danger">{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
const detailedDataBlocked = {
|
||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
encryption_status: statusBlocked,
|
||||
domain,
|
||||
type_table_header: type,
|
||||
protocol,
|
||||
known_tracker: 'title',
|
||||
table_name: hasTracker && tracker.name,
|
||||
category_label: hasTracker && tracker.category,
|
||||
source_label: hasTracker && source
|
||||
&& <a href={`//${source}`} className="link--green">{source}</a>,
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_table_header: response && response.join('\n'),
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className="title--border">{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
const detailedDataCurrent = isFiltered ? detailedDataBlocked : detailedData;
|
||||
|
||||
setDetailedDataCurrent(detailedDataCurrent);
|
||||
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;
|
||||
44
client/src/components/Logs/Tooltip/CustomTooltip.js
Normal file
44
client/src/components/Logs/Tooltip/CustomTooltip.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import Tooltip from './index';
|
||||
|
||||
const CustomTooltip = ({
|
||||
id, title, className, contentItemClass, place = 'right', columnClass = '', content, trigger, overridePosition, scrollHide,
|
||||
renderContent = React.Children.map(
|
||||
content,
|
||||
(item, idx) => <div key={idx} className={contentItemClass}>
|
||||
<Trans>{item || '—'}</Trans>
|
||||
</div>,
|
||||
),
|
||||
}) => <Tooltip id={id} className={className} place={place} trigger={trigger}
|
||||
overridePosition={overridePosition}
|
||||
scrollHide={scrollHide}
|
||||
>
|
||||
{title
|
||||
&& <div className="pb-4 h-25 grid-content font-weight-bold"><Trans>{title}</Trans></div>}
|
||||
<div className={classNames(columnClass)}>{renderContent}</div>
|
||||
</Tooltip>;
|
||||
|
||||
CustomTooltip.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
place: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
columnClass: PropTypes.string,
|
||||
contentItemClass: PropTypes.string,
|
||||
overridePosition: PropTypes.func,
|
||||
scrollHide: PropTypes.bool,
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.array,
|
||||
]),
|
||||
trigger: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||
};
|
||||
|
||||
export default CustomTooltip;
|
||||
129
client/src/components/Logs/Tooltip/ReactTooltip.css
Normal file
129
client/src/components/Logs/Tooltip/ReactTooltip.css
Normal file
@@ -0,0 +1,129 @@
|
||||
.custom-tooltip {
|
||||
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;
|
||||
}
|
||||
|
||||
/*crutch, may cause problems https://github.com/wwayne/react-tooltip/issues/204*/
|
||||
@media (hover: none) {
|
||||
.custom-tooltip {
|
||||
position: absolute !important;
|
||||
top: 4rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.white-space--nowrap {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.white-space--normal {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.word-break--break-all {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
.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: bold;
|
||||
}
|
||||
|
||||
.grid--title:not(:first-child) {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@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, .grid .title--border {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.custom-tooltip {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.grid .key-colon:nth-child(odd)::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.grid__one-row {
|
||||
grid-template-columns: 15rem;
|
||||
}
|
||||
|
||||
.grid__flow-column {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.custom-tooltip.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.custom-tooltip:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.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: 2rem;
|
||||
}
|
||||
|
||||
.title--border:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-top: 0.5px solid var(--gray-d8) !important;
|
||||
width: 100%;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
.icon-cross {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
48
client/src/components/Logs/Tooltip/index.js
Normal file
48
client/src/components/Logs/Tooltip/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import './ReactTooltip.css';
|
||||
import { touchMediaQuery } from '../../../helpers/constants';
|
||||
|
||||
const Tooltip = ({
|
||||
id, children, className = '', place = 'right', trigger = 'hover', overridePosition, scrollHide = true,
|
||||
}) => {
|
||||
const tooltipClassName = classNames('custom-tooltip', className);
|
||||
|
||||
return (
|
||||
<ReactTooltip
|
||||
id={id}
|
||||
aria-haspopup="true"
|
||||
effect="solid"
|
||||
place={place}
|
||||
className={tooltipClassName}
|
||||
backgroundColor="#fff"
|
||||
arrowColor="transparent"
|
||||
textColor="#4d4d4d"
|
||||
delayHide={300}
|
||||
scrollHide={window.matchMedia(touchMediaQuery).matches ? false : scrollHide}
|
||||
trigger={trigger}
|
||||
overridePosition={overridePosition}
|
||||
globalEventOff="click touchend"
|
||||
clickable
|
||||
>
|
||||
{children}
|
||||
</ReactTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
place: PropTypes.string,
|
||||
overridePosition: PropTypes.func,
|
||||
scrollHide: PropTypes.bool,
|
||||
trigger: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
1
client/src/components/Logs/chevron-down.svg
Normal file
1
client/src/components/Logs/chevron-down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 264 B |
@@ -1,457 +1,210 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { HashLink as Link } from 'react-router-hash-link';
|
||||
|
||||
import { Trans } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
formatTime,
|
||||
formatDateTime,
|
||||
isToday,
|
||||
checkFiltered,
|
||||
checkRewrite,
|
||||
checkRewriteHosts,
|
||||
checkWhiteList,
|
||||
checkBlackList,
|
||||
checkBlockedService,
|
||||
} from '../../helpers/helpers';
|
||||
import {
|
||||
SERVICES, TABLE_DEFAULT_PAGE_SIZE, CUSTOM_FILTERING_RULES_ID, FILTERED,
|
||||
BLOCK_ACTIONS, smallScreenSize,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
} from '../../helpers/constants';
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import { formatClientCell } from '../../helpers/formatClientCell';
|
||||
|
||||
import Filters from './Filters';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
import Loading from '../ui/Loading';
|
||||
import PopoverFiltered from '../ui/PopoverFilter';
|
||||
import Popover from '../ui/Popover';
|
||||
import Filters from './Filters';
|
||||
import Table from './Table';
|
||||
import Disabled from './Disabled';
|
||||
import './Logs.css';
|
||||
import CellWrap from '../ui/CellWrap';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import { getLogsConfig } from '../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../actions/toasts';
|
||||
|
||||
const TABLE_FIRST_PAGE = 0;
|
||||
const INITIAL_REQUEST = true;
|
||||
const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST];
|
||||
|
||||
class Logs extends Component {
|
||||
componentDidMount() {
|
||||
this.props.setLogsPage(TABLE_FIRST_PAGE);
|
||||
this.getLogs(...INITIAL_REQUEST_DATA);
|
||||
this.props.getFilteringStatus();
|
||||
this.props.getLogsConfig();
|
||||
}
|
||||
export const processContent = (data, buttonType) => Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
const isTitle = value === 'title';
|
||||
const isButton = key === buttonType;
|
||||
const isBoolean = typeof value === 'boolean';
|
||||
const isHidden = isBoolean && value === false;
|
||||
|
||||
getLogs = (older_than, page, initial) => {
|
||||
if (this.props.queryLogs.enabled) {
|
||||
this.props.getLogs({
|
||||
older_than, page, pageSize: TABLE_DEFAULT_PAGE_SIZE, initial,
|
||||
let keyClass = 'key-colon';
|
||||
|
||||
if (isTitle) {
|
||||
keyClass = 'title--border';
|
||||
}
|
||||
if (isButton || isBoolean) {
|
||||
keyClass = '';
|
||||
}
|
||||
|
||||
return isHidden ? null : <Fragment key={key}>
|
||||
<div
|
||||
className={`key__${key} ${keyClass} ${(isBoolean && value === true) ? 'font-weight-bold' : ''}`}>
|
||||
<Trans>{isButton ? value : key}</Trans>
|
||||
</div>
|
||||
<div className={`value__${key} text-pre text-truncate`}>
|
||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||
</div>
|
||||
</Fragment>;
|
||||
});
|
||||
|
||||
|
||||
const Logs = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize);
|
||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const {
|
||||
filtering,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
setLogsFilter,
|
||||
toggleDetailedLogs,
|
||||
dashboard,
|
||||
dnsConfig,
|
||||
queryLogs: {
|
||||
filter,
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
oldest,
|
||||
logs,
|
||||
pages,
|
||||
page,
|
||||
isDetailed,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${smallScreenSize}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
toggleDetailedLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mediaQuery.addListener(mediaQueryHandler);
|
||||
|
||||
return () => mediaQuery.removeListener(mediaQueryHandler);
|
||||
}, []);
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
const getLogs = (older_than, page, initial) => {
|
||||
if (props.queryLogs.enabled) {
|
||||
props.getLogs({
|
||||
older_than,
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
initial,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
refreshLogs = () => {
|
||||
window.location.reload();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(getClients());
|
||||
try {
|
||||
await Promise.all([
|
||||
getLogs(...INITIAL_REQUEST_DATA),
|
||||
dispatch(getLogsConfig()),
|
||||
dispatch(getDnsConfig()),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await Promise.all([
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||
getLogs(...INITIAL_REQUEST_DATA),
|
||||
]);
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
renderTooltip = (isFiltered, rule, filter, service) => isFiltered
|
||||
&& <PopoverFiltered rule={rule} filter={filter} service={service} />;
|
||||
|
||||
renderResponseList = (response, status) => {
|
||||
if (response.length > 0) {
|
||||
const listItems = response.map((response, index) => (
|
||||
<li key={index} title={response} className="logs__list-item">
|
||||
{response}
|
||||
</li>
|
||||
));
|
||||
|
||||
return <ul className="list-unstyled">{listItems}</ul>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Trans values={{ value: status }}>query_log_response_status</Trans>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
toggleBlocking = (type, domain) => {
|
||||
const { userRules } = this.props.filtering;
|
||||
const { t } = this.props;
|
||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||
const baseRule = `||${domain}^$important`;
|
||||
const baseUnblocking = `@@${baseRule}`;
|
||||
const blockingRule = type === 'block' ? baseUnblocking : baseRule;
|
||||
const unblockingRule = type === 'block' ? baseRule : baseUnblocking;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
if (userRules.match(preparedBlockingRule)) {
|
||||
this.props.setRules(userRules.replace(`${blockingRule}`, ''));
|
||||
this.props.addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
} else if (!userRules.match(preparedUnblockingRule)) {
|
||||
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
||||
this.props.addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
}
|
||||
|
||||
this.props.getFilteringStatus();
|
||||
};
|
||||
|
||||
renderBlockingButton(isFiltered, domain) {
|
||||
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
|
||||
const buttonText = isFiltered ? 'unblock_btn' : 'block_btn';
|
||||
const buttonType = isFiltered ? 'unblock' : 'block';
|
||||
|
||||
return (
|
||||
<div className="logs__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={() => this.toggleBlocking(buttonType, domain)}
|
||||
disabled={this.props.filtering.processingRules}
|
||||
>
|
||||
<Trans>{buttonText}</Trans>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getDateCell = (row) => CellWrap(
|
||||
row,
|
||||
(isToday(row.value) ? formatTime : formatDateTime),
|
||||
formatDateTime,
|
||||
return (
|
||||
<>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<Fragment>
|
||||
<Filters
|
||||
filter={filter}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
setLogsFilter={setLogsFilter}
|
||||
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--small icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</Fragment>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<Disabled />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
getDomainCell = (row) => {
|
||||
const response = row.value;
|
||||
const trackerData = getTrackerData(response);
|
||||
|
||||
return (
|
||||
<div className="logs__row" title={response}>
|
||||
<div className="logs__text">{response}</div>
|
||||
{trackerData && <Popover data={trackerData} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
normalizeResponse = (response) => (
|
||||
response.map((response) => {
|
||||
const { value, type, ttl } = response;
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
})
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
getResponseCell = ({ value: responses, original }) => {
|
||||
const {
|
||||
reason, filterId, rule, status, originalAnswer,
|
||||
} = original;
|
||||
const { t, filtering } = this.props;
|
||||
const { filters, whitelistFilters } = filtering;
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
const isBlackList = checkBlackList(reason);
|
||||
const isRewrite = checkRewrite(reason);
|
||||
const isRewriteAuto = checkRewriteHosts(reason);
|
||||
const isWhiteList = checkWhiteList(reason);
|
||||
const isBlockedService = checkBlockedService(reason);
|
||||
const isBlockedCnameIp = originalAnswer;
|
||||
|
||||
const filterKey = reason.replace(FILTERED, '');
|
||||
const parsedFilteredReason = t('query_log_filtered', { filter: filterKey });
|
||||
const currentService = SERVICES.find((service) => service.id === original.serviceName);
|
||||
const serviceName = currentService && currentService.name;
|
||||
const filterName = this.getFilterName(filters, whitelistFilters, filterId, t);
|
||||
|
||||
if (isBlockedCnameIp) {
|
||||
const normalizedAnswer = this.normalizeResponse(originalAnswer);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--column">
|
||||
<div className="logs__text-wrap">
|
||||
<span className="logs__text">
|
||||
<Trans>blocked_by_response</Trans>
|
||||
</span>
|
||||
{this.renderTooltip(isFiltered, rule, filterName)}
|
||||
</div>
|
||||
<div className="logs__list-wrap">
|
||||
{this.renderResponseList(normalizedAnswer, status)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--column">
|
||||
<div className="logs__text-wrap">
|
||||
{(isFiltered || isBlockedService) && !isBlackList && (
|
||||
<span className="logs__text" title={parsedFilteredReason}>
|
||||
{parsedFilteredReason}
|
||||
</span>
|
||||
)}
|
||||
{isBlackList && (
|
||||
<span className="logs__text">
|
||||
<Trans values={{ filter: filterName }}>
|
||||
query_log_filtered
|
||||
</Trans>
|
||||
</span>
|
||||
)}
|
||||
{isBlockedService
|
||||
? this.renderTooltip(isFiltered, '', '', serviceName)
|
||||
: this.renderTooltip(isFiltered, rule, filterName)}
|
||||
{isRewrite && (
|
||||
<strong>
|
||||
<Trans>rewrite_applied</Trans>
|
||||
</strong>
|
||||
)}
|
||||
{isRewriteAuto && (
|
||||
<span className="logs__text">
|
||||
<strong>
|
||||
<Trans>rewrite_hosts_applied</Trans>
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="logs__list-wrap">
|
||||
{this.renderResponseList(responses, status)}
|
||||
{isWhiteList && this.renderTooltip(isWhiteList, rule, filterName)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getClientCell = (row) => {
|
||||
const { original } = row;
|
||||
const { t } = this.props;
|
||||
const { reason, domain } = original;
|
||||
const isFiltered = checkFiltered(reason);
|
||||
const isRewrite = checkRewrite(reason);
|
||||
const isAutoRewrite = checkRewriteHosts(reason);
|
||||
|
||||
if (isAutoRewrite) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow logs__row--column">
|
||||
{formatClientCell(row, t)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="logs__row logs__row--overflow logs__row--column">
|
||||
{formatClientCell(row, t)}
|
||||
</div>
|
||||
{isRewrite ? (
|
||||
<div className="logs__action">
|
||||
<Link to="/dns_rewrites" className="btn btn-sm btn-outline-primary">
|
||||
<Trans>configure</Trans>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
this.renderBlockingButton(isFiltered, domain)
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
fetchData = (state) => {
|
||||
const { pages } = state;
|
||||
const { oldest, page } = this.props.queryLogs;
|
||||
const isLastPage = pages && (page + 1 === pages);
|
||||
|
||||
if (isLastPage) {
|
||||
this.getLogs(oldest, page);
|
||||
}
|
||||
};
|
||||
|
||||
changePage = (page) => {
|
||||
this.props.setLogsPage(page);
|
||||
this.props.setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE });
|
||||
};
|
||||
|
||||
renderLogs() {
|
||||
const { queryLogs, t } = this.props;
|
||||
const {
|
||||
processingGetLogs, processingGetConfig, logs, pages, page,
|
||||
} = queryLogs;
|
||||
const isLoading = processingGetLogs || processingGetConfig;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
minWidth: 105,
|
||||
Cell: this.getDateCell,
|
||||
},
|
||||
{
|
||||
Header: t('domain_name_table_header'),
|
||||
accessor: 'domain',
|
||||
minWidth: 180,
|
||||
Cell: this.getDomainCell,
|
||||
},
|
||||
{
|
||||
Header: t('type_table_header'),
|
||||
accessor: 'type',
|
||||
maxWidth: 60,
|
||||
},
|
||||
{
|
||||
Header: t('response_table_header'),
|
||||
accessor: 'response',
|
||||
minWidth: 250,
|
||||
Cell: this.getResponseCell,
|
||||
},
|
||||
{
|
||||
Header: t('client_table_header'),
|
||||
accessor: 'client',
|
||||
maxWidth: 240,
|
||||
minWidth: 240,
|
||||
Cell: this.getClientCell,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ReactTable
|
||||
manual
|
||||
minRows={5}
|
||||
page={page}
|
||||
pages={pages}
|
||||
columns={columns}
|
||||
filterable={false}
|
||||
sortable={false}
|
||||
data={logs || []}
|
||||
loading={isLoading}
|
||||
showPagination={true}
|
||||
showPaginationTop={true}
|
||||
showPageJump={false}
|
||||
showPageSizeOptions={false}
|
||||
onFetchData={this.fetchData}
|
||||
onPageChange={this.changePage}
|
||||
className="logs__table"
|
||||
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
loadingText={t('loading_table_status')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('no_logs_found')}
|
||||
pageText={''}
|
||||
ofText={''}
|
||||
renderTotalPagesCount={() => false}
|
||||
defaultFilterMethod={(filter, row) => {
|
||||
const id = filter.pivotId || filter.id;
|
||||
return row[id] !== undefined
|
||||
? String(row[id]).indexOf(filter.value) !== -1
|
||||
: true;
|
||||
}}
|
||||
defaultSorted={[
|
||||
{
|
||||
id: 'time',
|
||||
desc: true,
|
||||
},
|
||||
]}
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { reason } = rowInfo.original;
|
||||
|
||||
if (checkFiltered(reason)) {
|
||||
return {
|
||||
className: 'red',
|
||||
};
|
||||
} if (checkWhiteList(reason)) {
|
||||
return {
|
||||
className: 'green',
|
||||
};
|
||||
} if (checkRewrite(reason) || checkRewriteHosts(reason)) {
|
||||
return {
|
||||
className: 'blue',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: '',
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { queryLogs, t } = this.props;
|
||||
const {
|
||||
enabled, processingGetConfig, processingAdditionalLogs, processingGetLogs,
|
||||
} = queryLogs;
|
||||
|
||||
const refreshButton = enabled ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm ml-3"
|
||||
onClick={this.refreshLogs}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('query_log')}>{refreshButton}</PageTitle>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<Fragment>
|
||||
<Filters
|
||||
filter={queryLogs.filter}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
setLogsFilter={this.props.setLogsFilter}
|
||||
/>
|
||||
<Card>{this.renderLogs()}</Card>
|
||||
</Fragment>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Logs.propTypes = {
|
||||
getLogs: PropTypes.func.isRequired,
|
||||
@@ -461,12 +214,11 @@ Logs.propTypes = {
|
||||
filtering: PropTypes.object.isRequired,
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
getClients: PropTypes.func.isRequired,
|
||||
getLogsConfig: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
setLogsFilter: PropTypes.func.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(Logs);
|
||||
export default Logs;
|
||||
|
||||
@@ -79,16 +79,26 @@ class AutoClients extends Component {
|
||||
},
|
||||
]}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText="/"
|
||||
pageText=''
|
||||
ofText=''
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ class ClientsTable extends Component {
|
||||
const { value } = row;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text">
|
||||
{value.map((address) => (
|
||||
<div key={address} title={address}>
|
||||
@@ -121,7 +121,7 @@ class ClientsTable extends Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<div className="logs__text">{title}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -167,7 +167,7 @@ class ClientsTable extends Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<div className="logs__text">{title}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -185,7 +185,7 @@ class ClientsTable extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text">
|
||||
{value.map((tag) => (
|
||||
<div key={tag} title={tag} className="small">
|
||||
@@ -282,16 +282,26 @@ class ClientsTable extends Component {
|
||||
},
|
||||
]}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--small icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText="/"
|
||||
pageText=''
|
||||
ofText=''
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -89,7 +89,7 @@ const renderFieldsWrapper = (placeholder, buttonTitle) => function cell(row) {
|
||||
onClick={() => fields.push()}
|
||||
title={buttonTitle}
|
||||
>
|
||||
<svg className="icon icon--close">
|
||||
<svg className="icon icon--small">
|
||||
<use xlinkHref="#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -32,11 +32,9 @@ const getFormattedWhois = (value, t) => {
|
||||
const whoisCell = (t) => function cell(row) {
|
||||
const { value } = row;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</span>
|
||||
</div>
|
||||
);
|
||||
return <div className="logs__row o-hidden">
|
||||
<div className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default whoisCell;
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { SMALL_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
|
||||
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
|
||||
|
||||
class Leases extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
@@ -37,9 +37,9 @@ class Leases extends Component {
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
]}
|
||||
pageSize={SMALL_TABLE_DEFAULT_PAGE_SIZE}
|
||||
pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
||||
showPageSizeOptions={false}
|
||||
showPagination={leases.length > SMALL_TABLE_DEFAULT_PAGE_SIZE}
|
||||
showPagination={leases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
||||
noDataText={t('dhcp_leases_not_found')}
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
|
||||
@@ -2,13 +2,13 @@ import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { SMALL_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants';
|
||||
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants';
|
||||
|
||||
import Modal from './Modal';
|
||||
|
||||
class StaticLeases extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
@@ -67,7 +67,7 @@ class StaticLeases extends Component {
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
className="btn btn-icon btn-icon--green btn-outline-secondary btn-sm"
|
||||
title={t('delete_table_action')}
|
||||
disabled={processingDeleting}
|
||||
onClick={() => this.handleDelete(ip, mac, hostname)
|
||||
@@ -82,9 +82,9 @@ class StaticLeases extends Component {
|
||||
},
|
||||
},
|
||||
]}
|
||||
pageSize={SMALL_TABLE_DEFAULT_PAGE_SIZE}
|
||||
pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
||||
showPageSizeOptions={false}
|
||||
showPagination={staticLeases.length > SMALL_TABLE_DEFAULT_PAGE_SIZE}
|
||||
showPagination={staticLeases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
||||
noDataText={t('dhcp_static_leases_not_found')}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
minRows={6}
|
||||
|
||||
@@ -112,6 +112,11 @@
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-icon--green {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
width: 345px;
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
.toasts {
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants';
|
||||
|
||||
class Toast extends Component {
|
||||
componentDidMount() {
|
||||
const timeout = this.props.type === 'success' ? 5000 : 30000;
|
||||
state = {
|
||||
timerId: null,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
}, timeout);
|
||||
componentDidMount() {
|
||||
this.setRemoveToastTimeout();
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearRemoveToastTimeout = () => clearTimeout(this.state.timerId);
|
||||
|
||||
setRemoveToastTimeout = () => {
|
||||
const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT;
|
||||
|
||||
const timerId = setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
}, timeout);
|
||||
|
||||
this.setState({ timerId });
|
||||
};
|
||||
|
||||
showMessage(t, type, message) {
|
||||
if (type === 'notice') {
|
||||
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
|
||||
@@ -29,12 +42,18 @@ class Toast extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={`toast toast--${type}`}>
|
||||
<div className={`toast toast--${type}`}
|
||||
onMouseOver={this.clearRemoveToastTimeout}
|
||||
onMouseOut={this.setRemoveToastTimeout}>
|
||||
<p className="toast__content">
|
||||
{this.showMessage(t, type, message)}
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 6-12 12" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ const CellWrap = ({ value }, formatValue, formatTitle = formatValue) => {
|
||||
const cellTitle = typeof formatTitle === 'function' ? formatTitle(value) : value;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text logs__text--full" title={cellTitle}>
|
||||
{cellValue}
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,19 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon--close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.icon--small {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.icon--gray {
|
||||
color: var(--gray-a5);
|
||||
}
|
||||
|
||||
.icon--disabled {
|
||||
color: var(--gray-d8);
|
||||
}
|
||||
|
||||
.icon--active {
|
||||
color: #66b574;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import './Loading.css';
|
||||
|
||||
const Loading = () => (
|
||||
<div className="loading"></div>
|
||||
<div className="loading" />
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -3,13 +3,32 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header--logs {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
margin: 2rem 0 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.page-header--logs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header--logs .page-title {
|
||||
padding-bottom: 2.5rem;;
|
||||
}
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin-left: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
line-height: 2.2rem;
|
||||
.page-title--large {
|
||||
font-size: 36px;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.page-title__actions {
|
||||
|
||||
@@ -3,15 +3,15 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import './PageTitle.css';
|
||||
|
||||
const PageTitle = (props) => (
|
||||
const PageTitle = ({ title, subtitle, children }) => (
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
{props.title}
|
||||
{props.children}
|
||||
{title}
|
||||
{children}
|
||||
</h1>
|
||||
{props.subtitle && (
|
||||
{subtitle && (
|
||||
<div className="page-subtitle">
|
||||
{props.subtitle}
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
.ReactTable .rt-th,
|
||||
.ReactTable .rt-td {
|
||||
padding: 10px 15px;
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rt-tr-group .red {
|
||||
background-color: #fff4f2;
|
||||
.rt-tr-group.red {
|
||||
background-color: rgba(223, 56, 18, 0.05);
|
||||
}
|
||||
|
||||
.rt-tr-group .green {
|
||||
background-color: #f1faf3;
|
||||
.rt-tr-group.green {
|
||||
background-color: rgba(103, 178, 121, 0.1);
|
||||
}
|
||||
|
||||
.rt-tr-group .blue {
|
||||
background-color: #ecf7ff;
|
||||
.rt-tr-group.blue {
|
||||
background-color: #e5effd;
|
||||
}
|
||||
|
||||
.rt-tr-group.yellow {
|
||||
background-color: var(--yellow-pale);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 5px;
|
||||
background-image: url("./svg/help-circle.svg");
|
||||
background-size: 100%;
|
||||
cursor: pointer;
|
||||
@@ -56,7 +55,6 @@
|
||||
.tooltip-custom--logs {
|
||||
border-radius: 50%;
|
||||
background-image: url("./svg/help-circle-gray.svg");
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.tooltip-custom--logs:before {
|
||||
|
||||
@@ -3,9 +3,8 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import './Tooltip.css';
|
||||
|
||||
const Tooltip = (props) => (
|
||||
<div data-tooltip={props.text} className={`tooltip-custom ${props.type || ''}`}></div>
|
||||
);
|
||||
const Tooltip = ({ text, type = '' }) => <div data-tooltip={text}
|
||||
className={`tooltip-custom ml-1 ${type}`} />;
|
||||
|
||||
Tooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
|
||||
Reference in New Issue
Block a user