Merge: + client: add response information to the filtered query log items

Closes #912

* commit '428706399ae5bcd01daf4b4146c81a481fd680ef':
  + client: add response information to the filtered query log items
This commit is contained in:
Ildar Kamalov 2019-09-04 10:10:39 +03:00
commit 47e5fcb14c
5 changed files with 315 additions and 249 deletions

View File

@ -176,6 +176,8 @@
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules", "rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules",
"query_log_disabled_toast": "Query log disabled", "query_log_disabled_toast": "Query log disabled",
"query_log_enabled_toast": "Query log enabled", "query_log_enabled_toast": "Query log enabled",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtered by {{filter}}",
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",

View File

@ -9,13 +9,12 @@
justify-content: center; justify-content: center;
} }
.logs__row--overflow { .logs__row--column {
overflow: hidden; align-items: flex-start;
flex-direction: column;
} }
.logs__row--column { .logs__row--overflow {
flex-direction: column;
align-items: flex-start;
overflow: hidden; overflow: hidden;
} }
@ -103,3 +102,20 @@
border-color: #1991eb; border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25); box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
} }
.logs__text-wrap {
display: flex;
align-items: center;
max-width: 100%;
}
.logs__list-wrap {
display: flex;
max-width: 100%;
}
.logs__list-item {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

View File

@ -8,7 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
import { formatTime, getClientName } from '../../helpers/helpers'; import { formatTime, getClientName } from '../../helpers/helpers';
import { SERVICES } from '../../helpers/constants'; import { SERVICES, FILTERED_STATUS } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers'; import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -18,6 +18,11 @@ import Popover from '../ui/Popover';
import './Logs.css'; import './Logs.css';
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt'; const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
const FILTERED_REASON = 'Filtered';
const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};
class Logs extends Component { class Logs extends Component {
componentDidMount() { componentDidMount() {
@ -38,11 +43,29 @@ class Logs extends Component {
if (this.props.dashboard.queryLogEnabled) { if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs(); this.props.getLogs();
} }
} };
renderTooltip = (isFiltered, rule, filter, service) => renderTooltip = (isFiltered, rule, filter, service) =>
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={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) => { toggleBlocking = (type, domain) => {
const { userRules } = this.props.filtering; const { userRules } = this.props.filtering;
const { t } = this.props; const { t } = this.props;
@ -63,7 +86,7 @@ class Logs extends Component {
} }
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} };
renderBlockingButton(isFiltered, domain) { renderBlockingButton(isFiltered, domain) {
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger'; const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
@ -84,228 +107,244 @@ class Logs extends Component {
); );
} }
checkFiltered = reason => reason.indexOf(FILTERED_REASON) === 0;
checkRewrite = reason => reason === FILTERED_STATUS.REWRITE;
checkWhiteList = reason => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;
getTimeCell = ({ value }) => (
<div className="logs__row">
<span className="logs__text" title={value}>
{formatTime(value)}
</span>
</div>
);
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>
);
};
getResponseCell = ({ value: responses, original }) => {
const {
reason, filterId, rule, status,
} = original;
const { t, filtering } = this.props;
const { filters } = filtering;
const isFiltered = this.checkFiltered(reason);
const filterKey = reason.replace(FILTERED_REASON, '');
const parsedFilteredReason = t('query_log_filtered', { filter: filterKey });
const isRewrite = this.checkRewrite(reason);
const isWhiteList = this.checkWhiteList(reason);
const isBlockedService = reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const currentService = SERVICES.find(service => service.id === original.serviceName);
const serviceName = currentService && currentService.name;
let filterName = '';
if (filterId === 0) {
filterName = t('custom_filter_rules');
} else {
const filterItem = Object.keys(filters).filter(key => filters[key].id === filterId)[0];
if (
typeof filterItem !== 'undefined' &&
typeof filters[filterItem] !== 'undefined'
) {
filterName = filters[filterItem].name;
}
if (!filterName) {
filterName = t('unknown_filter', { filterId });
}
}
return (
<div className="logs__row logs__row--column">
<div className="logs__text-wrap">
{(isFiltered || isBlockedService) && (
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
)}
{isBlockedService
? this.renderTooltip(isFiltered, '', '', serviceName)
: this.renderTooltip(isFiltered, rule, filterName)}
{isRewrite && (
<strong>
<Trans>rewrite_applied</Trans>
</strong>
)}
</div>
<div className="logs__list-wrap">
{this.renderResponseList(responses, status)}
{isWhiteList && this.renderTooltip(isWhiteList, rule, filterName)}
</div>
</div>
);
};
getClientCell = ({ original, value }) => {
const { dashboard } = this.props;
const { reason, domain } = original;
const isFiltered = this.checkFiltered(reason);
const isRewrite = this.checkRewrite(reason);
const clientName =
getClientName(dashboard.clients, value) || getClientName(dashboard.autoClients, value);
let client = value;
if (clientName) {
client = (
<span>
{clientName} <small>({value})</small>
</span>
);
}
return (
<Fragment>
<div className="logs__row">{client}</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>
);
};
renderLogs(logs) { renderLogs(logs) {
const { t, dashboard } = this.props; const { t } = this.props;
const columns = [{ const columns = [
Header: t('time_table_header'), {
accessor: 'time', Header: t('time_table_header'),
maxWidth: 110, accessor: 'time',
filterable: false, maxWidth: 90,
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>), filterable: false,
}, { Cell: this.getTimeCell,
Header: t('domain_name_table_header'),
accessor: 'domain',
Cell: (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>
);
}, },
}, { {
Header: t('type_table_header'), Header: t('domain_name_table_header'),
accessor: 'type', accessor: 'domain',
maxWidth: 60, minWidth: 180,
}, { Cell: this.getDomainCell,
Header: t('response_table_header'), },
accessor: 'response', {
Cell: (row) => { Header: t('type_table_header'),
const responses = row.value; accessor: 'type',
const { reason } = row.original; maxWidth: 60,
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false; },
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by '); {
const rule = row && row.original && row.original.rule; Header: t('response_table_header'),
const { filterId } = row.original; accessor: 'response',
const { filters } = this.props.filtering; minWidth: 250,
const isRewrite = reason && reason === 'Rewrite'; Cell: this.getResponseCell,
let filterName = ''; filterMethod: (filter, row) => {
if (filter.value === RESPONSE_FILTER.FILTERED) {
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') { // eslint-disable-next-line no-underscore-dangle
if (filterId === 0) { const { reason } = row._original;
filterName = t('custom_filter_rules'); return (
} else { this.checkFiltered(reason) ||
const filterItem = Object.keys(filters) this.checkWhiteList(reason)
.filter(key => filters[key].id === filterId); );
if (typeof filterItem !== 'undefined' && typeof filters[filterItem] !== 'undefined') {
filterName = filters[filterItem].name;
}
if (!filterName) {
filterName = t('unknown_filter', { filterId });
}
} }
} return true;
},
if (reason === 'FilteredBlockedService') { Filter: ({ filter, onChange }) => (
const getService = SERVICES <select
.find(service => service.id === row.original.serviceName); className="form-control"
const serviceName = getService && getService.name; onChange={event => onChange(event.target.value)}
value={filter ? filter.value : RESPONSE_FILTER.ALL}
return ( >
<div className="logs__row"> <option value={RESPONSE_FILTER.ALL}>
<span className="logs__text" title={parsedFilteredReason}> <Trans>show_all_filter_type</Trans>
{parsedFilteredReason} </option>
</span> <option value={RESPONSE_FILTER.FILTERED}>
{this.renderTooltip(isFiltered, '', '', serviceName)} <Trans>show_filtered_type</Trans>
</div> </option>
); </select>
} ),
if (isFiltered) {
return (
<div className="logs__row">
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
);
}
if (responses.length > 0) {
const liNodes = responses.map((response, index) =>
(<li key={index} title={response}>{response}</li>));
const isRenderTooltip = reason === 'NotFilteredWhiteList';
return (
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<ul className="list-unstyled">{liNodes}</ul>
{this.renderTooltip(isRenderTooltip, rule, filterName)}
</div>
);
}
return (
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<span><Trans>empty_response_status</Trans></span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
);
}, },
filterMethod: (filter, row) => { {
if (filter.value === 'filtered') { Header: t('client_table_header'),
// eslint-disable-next-line no-underscore-dangle accessor: 'client',
return row._original.reason.indexOf('Filtered') === 0 || row._original.reason === 'NotFilteredWhiteList'; maxWidth: 220,
} minWidth: 220,
return true; Cell: this.getClientCell,
}, },
Filter: ({ filter, onChange }) =>
<select
onChange={event => onChange(event.target.value)}
className="form-control"
value={filter ? filter.value : 'all'}
>
<option value="all">{ t('show_all_filter_type') }</option>
<option value="filtered">{ t('show_filtered_type') }</option>
</select>,
}, {
Header: t('client_table_header'),
accessor: 'client',
maxWidth: 250,
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const isRewrite = reason && reason === 'Rewrite';
const clientName = getClientName(dashboard.clients, row.value)
|| getClientName(dashboard.autoClients, row.value);
let client;
if (clientName) {
client = <span>{clientName} <small>({row.value})</small></span>;
} else {
client = row.value;
}
if (isRewrite) {
return (
<Fragment>
<div className="logs__row">
{client}
</div>
<div className="logs__action">
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
<Trans>configure</Trans>
</Link>
</div>
</Fragment>
);
}
return (
<Fragment>
<div className="logs__row">
{client}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
);
},
},
]; ];
if (logs) { if (logs) {
return (<ReactTable return (
className='logs__table' <ReactTable
filterable className="logs__table"
data={logs} filterable
columns={columns} data={logs}
showPagination={true} columns={columns}
defaultPageSize={50} showPagination={true}
minRows={7} defaultPageSize={50}
// Text minRows={7}
previousText={ t('previous_btn') } previousText={t('previous_btn')}
nextText={ t('next_btn') } nextText={t('next_btn')}
loadingText={ t('loading_table_status') } loadingText={t('loading_table_status')}
pageText={ t('page_table_footer_text') } pageText={t('page_table_footer_text')}
ofText={ t('of_table_footer_text') } ofText={t('of_table_footer_text')}
rowsText={ t('rows_table_footer_text') } rowsText={t('rows_table_footer_text')}
noDataText={ t('no_logs_found') } noDataText={t('no_logs_found')}
defaultFilterMethod={(filter, row) => { defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id; const id = filter.pivotId || filter.id;
return row[id] !== undefined ? return row[id] !== undefined
String(row[id]).indexOf(filter.value) !== -1 : true; ? String(row[id]).indexOf(filter.value) !== -1
}} : true;
defaultSorted={[ }}
{ defaultSorted={[
id: 'time', {
desc: true, id: 'time',
}, desc: true,
]} },
getTrProps={(_state, rowInfo) => { ]}
// highlight filtered requests getTrProps={(_state, rowInfo) => {
if (!rowInfo) { if (!rowInfo) {
return {}; return {};
} }
if (rowInfo.original.reason.indexOf('Filtered') === 0) { const { reason } = rowInfo.original;
return {
className: 'red',
};
} else if (rowInfo.original.reason === 'NotFilteredWhiteList') {
return {
className: 'green',
};
} else if (rowInfo.original.reason === 'Rewrite') {
return {
className: 'blue',
};
}
return { if (this.checkFiltered(reason)) {
className: '', return {
}; className: 'red',
}} };
/>); } else if (this.checkWhiteList(reason)) {
return {
className: 'green',
};
} else if (this.checkRewrite(reason)) {
return {
className: 'blue',
};
}
return {
className: '',
};
}}
/>
);
} }
return undefined;
return null;
} }
handleDownloadButton = async (e) => { handleDownloadButton = async (e) => {
@ -325,17 +364,23 @@ class Logs extends Component {
type="submit" type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)} onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing} disabled={logStatusProcessing}
><Trans>disabled_log_btn</Trans></button> >
<Trans>disabled_log_btn</Trans>
</button>
<button <button
className="btn btn-primary btn-sm mr-2" className="btn btn-primary btn-sm mr-2"
type="submit" type="submit"
onClick={this.handleDownloadButton} onClick={this.handleDownloadButton}
><Trans>download_log_file_btn</Trans></button> >
<Trans>download_log_file_btn</Trans>
</button>
<button <button
className="btn btn-outline-primary btn-sm" className="btn btn-outline-primary btn-sm"
type="submit" type="submit"
onClick={this.getLogs} onClick={this.getLogs}
><Trans>refresh_btn</Trans></button> >
<Trans>refresh_btn</Trans>
</button>
</Fragment> </Fragment>
); );
} }
@ -346,7 +391,9 @@ class Logs extends Component {
type="submit" type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)} onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing} disabled={logStatusProcessing}
><Trans>enabled_log_btn</Trans></button> >
<Trans>enabled_log_btn</Trans>
</button>
); );
} }
@ -355,24 +402,19 @@ class Logs extends Component {
const { queryLogEnabled } = dashboard; const { queryLogEnabled } = dashboard;
return ( return (
<Fragment> <Fragment>
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }> <PageTitle title={t('query_log')} subtitle={t('last_dns_queries')}>
<div className="page-title__actions"> <div className="page-title__actions">
{this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)} {this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)}
</div> </div>
</PageTitle> </PageTitle>
<Card> <Card>
{ {queryLogEnabled &&
queryLogEnabled queryLogs.getLogsProcessing &&
&& queryLogs.getLogsProcessing dashboard.processingClients && <Loading />}
&& dashboard.processingClients {queryLogEnabled &&
&& <Loading /> !queryLogs.getLogsProcessing &&
} !dashboard.processingClients &&
{ this.renderLogs(queryLogs.logs)}
queryLogEnabled
&& !queryLogs.getLogsProcessing
&& !dashboard.processingClients
&& this.renderLogs(queryLogs.logs)
}
</Card> </Card>
</Fragment> </Fragment>
); );
@ -380,20 +422,17 @@ class Logs extends Component {
} }
Logs.propTypes = { Logs.propTypes = {
getLogs: PropTypes.func, getLogs: PropTypes.func.isRequired,
queryLogs: PropTypes.object, queryLogs: PropTypes.object.isRequired,
dashboard: PropTypes.object, dashboard: PropTypes.object.isRequired,
toggleLogStatus: PropTypes.func, toggleLogStatus: PropTypes.func.isRequired,
downloadQueryLog: PropTypes.func, downloadQueryLog: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func, getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object, filtering: PropTypes.object.isRequired,
userRules: PropTypes.string, setRules: PropTypes.func.isRequired,
setRules: PropTypes.func, addSuccessToast: PropTypes.func.isRequired,
addSuccessToast: PropTypes.func,
processingRules: PropTypes.bool,
logStatusProcessing: PropTypes.bool,
t: PropTypes.func,
getClients: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(Logs); export default withNamespaces()(Logs);

View File

@ -253,3 +253,10 @@ export const ENCRYPTION_SOURCE = {
PATH: 'path', PATH: 'path',
CONTENT: 'content', CONTENT: 'content',
}; };
export const FILTERED_STATUS = {
FILTERED_BLACK_LIST: 'FilteredBlackList',
NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
REWRITE: 'Rewrite',
};

View File

@ -28,6 +28,7 @@ export const normalizeLogs = logs => logs.map((log) => {
filterId, filterId,
rule, rule,
service_name, service_name,
status,
} = log; } = log;
const { host: domain, type } = question; const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => { const responsesArray = response ? response.map((response) => {
@ -44,6 +45,7 @@ export const normalizeLogs = logs => logs.map((log) => {
filterId, filterId,
rule, rule,
serviceName: service_name, serviceName: service_name,
status,
}; };
}); });