badguardhome/client/src/components/Logs/Table.js
Artem Baskal e39fe1b913 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: 9d9787fd b1c951fb
Author: 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: 98576823 15e71435
Author: 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
2020-06-18 00:36:19 +03:00

400 lines
15 KiB
JavaScript

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;