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",
"query_log_disabled_toast": "Query log disabled",
"query_log_enabled_toast": "Query log enabled",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtered by {{filter}}",
"source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category",

View File

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

View File

@ -8,7 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link';
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 PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
@ -18,6 +18,11 @@ import Popover from '../ui/Popover';
import './Logs.css';
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
const FILTERED_REASON = 'Filtered';
const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};
class Logs extends Component {
componentDidMount() {
@ -38,11 +43,29 @@ class Logs extends Component {
if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs();
}
}
};
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;
@ -63,7 +86,7 @@ class Logs extends Component {
}
this.props.getFilteringStatus();
}
};
renderBlockingButton(isFiltered, domain) {
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
@ -84,56 +107,58 @@ class Logs extends Component {
);
}
renderLogs(logs) {
const { t, dashboard } = this.props;
const columns = [{
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 110,
filterable: false,
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>),
}, {
Header: t('domain_name_table_header'),
accessor: 'domain',
Cell: (row) => {
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>
<div className="logs__text">{response}</div>
{trackerData && <Popover data={trackerData} />}
</div>
);
},
}, {
Header: t('type_table_header'),
accessor: 'type',
maxWidth: 60,
}, {
Header: t('response_table_header'),
accessor: 'response',
Cell: (row) => {
const responses = row.value;
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by ');
const rule = row && row.original && row.original.rule;
const { filterId } = row.original;
const { filters } = this.props.filtering;
const isRewrite = reason && reason === 'Rewrite';
};
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 (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
if (filterId === 0) {
filterName = t('custom_filter_rules');
} else {
const filterItem = Object.keys(filters)
.filter(key => filters[key].id === filterId);
const filterItem = Object.keys(filters).filter(key => filters[key].id === filterId)[0];
if (typeof filterItem !== 'undefined' && typeof filters[filterItem] !== 'undefined') {
if (
typeof filterItem !== 'undefined' &&
typeof filters[filterItem] !== 'undefined'
) {
filterName = filters[filterItem].name;
}
@ -141,126 +166,136 @@ class Logs extends Component {
filterName = t('unknown_filter', { filterId });
}
}
}
if (reason === 'FilteredBlockedService') {
const getService = SERVICES
.find(service => service.id === row.original.serviceName);
const serviceName = getService && getService.name;
return (
<div className="logs__row">
<div className="logs__row logs__row--column">
<div className="logs__text-wrap">
{(isFiltered || isBlockedService) && (
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
{this.renderTooltip(isFiltered, '', '', serviceName)}
)}
{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>
);
}
};
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') {
// eslint-disable-next-line no-underscore-dangle
return row._original.reason.indexOf('Filtered') === 0 || row._original.reason === 'NotFilteredWhiteList';
}
return true;
},
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;
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>({row.value})</small></span>;
} else {
client = row.value;
client = (
<span>
{clientName} <small>({value})</small>
</span>
);
}
if (isRewrite) {
return (
<Fragment>
<div className="logs__row">
{client}
</div>
<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) {
const { t } = this.props;
const columns = [
{
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 90,
filterable: false,
Cell: this.getTimeCell,
},
{
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,
filterMethod: (filter, row) => {
if (filter.value === RESPONSE_FILTER.FILTERED) {
// eslint-disable-next-line no-underscore-dangle
const { reason } = row._original;
return (
this.checkFiltered(reason) ||
this.checkWhiteList(reason)
);
}
return (
<Fragment>
<div className="logs__row">
{client}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
);
return true;
},
Filter: ({ filter, onChange }) => (
<select
className="form-control"
onChange={event => onChange(event.target.value)}
value={filter ? filter.value : RESPONSE_FILTER.ALL}
>
<option value={RESPONSE_FILTER.ALL}>
<Trans>show_all_filter_type</Trans>
</option>
<option value={RESPONSE_FILTER.FILTERED}>
<Trans>show_filtered_type</Trans>
</option>
</select>
),
},
{
Header: t('client_table_header'),
accessor: 'client',
maxWidth: 220,
minWidth: 220,
Cell: this.getClientCell,
},
];
if (logs) {
return (<ReactTable
className='logs__table'
return (
<ReactTable
className="logs__table"
filterable
data={logs}
columns={columns}
showPagination={true}
defaultPageSize={50}
minRows={7}
// Text
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
@ -270,8 +305,9 @@ class Logs extends Component {
noDataText={t('no_logs_found')}
defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id;
return row[id] !== undefined ?
String(row[id]).indexOf(filter.value) !== -1 : true;
return row[id] !== undefined
? String(row[id]).indexOf(filter.value) !== -1
: true;
}}
defaultSorted={[
{
@ -280,20 +316,21 @@ class Logs extends Component {
},
]}
getTrProps={(_state, rowInfo) => {
// highlight filtered requests
if (!rowInfo) {
return {};
}
if (rowInfo.original.reason.indexOf('Filtered') === 0) {
const { reason } = rowInfo.original;
if (this.checkFiltered(reason)) {
return {
className: 'red',
};
} else if (rowInfo.original.reason === 'NotFilteredWhiteList') {
} else if (this.checkWhiteList(reason)) {
return {
className: 'green',
};
} else if (rowInfo.original.reason === 'Rewrite') {
} else if (this.checkRewrite(reason)) {
return {
className: 'blue',
};
@ -303,9 +340,11 @@ class Logs extends Component {
className: '',
};
}}
/>);
/>
);
}
return undefined;
return null;
}
handleDownloadButton = async (e) => {
@ -325,17 +364,23 @@ class Logs extends Component {
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing}
><Trans>disabled_log_btn</Trans></button>
>
<Trans>disabled_log_btn</Trans>
</button>
<button
className="btn btn-primary btn-sm mr-2"
type="submit"
onClick={this.handleDownloadButton}
><Trans>download_log_file_btn</Trans></button>
>
<Trans>download_log_file_btn</Trans>
</button>
<button
className="btn btn-outline-primary btn-sm"
type="submit"
onClick={this.getLogs}
><Trans>refresh_btn</Trans></button>
>
<Trans>refresh_btn</Trans>
</button>
</Fragment>
);
}
@ -346,7 +391,9 @@ class Logs extends Component {
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing}
><Trans>enabled_log_btn</Trans></button>
>
<Trans>enabled_log_btn</Trans>
</button>
);
}
@ -361,18 +408,13 @@ class Logs extends Component {
</div>
</PageTitle>
<Card>
{
queryLogEnabled
&& queryLogs.getLogsProcessing
&& dashboard.processingClients
&& <Loading />
}
{
queryLogEnabled
&& !queryLogs.getLogsProcessing
&& !dashboard.processingClients
&& this.renderLogs(queryLogs.logs)
}
{queryLogEnabled &&
queryLogs.getLogsProcessing &&
dashboard.processingClients && <Loading />}
{queryLogEnabled &&
!queryLogs.getLogsProcessing &&
!dashboard.processingClients &&
this.renderLogs(queryLogs.logs)}
</Card>
</Fragment>
);
@ -380,20 +422,17 @@ class Logs extends Component {
}
Logs.propTypes = {
getLogs: PropTypes.func,
queryLogs: PropTypes.object,
dashboard: PropTypes.object,
toggleLogStatus: PropTypes.func,
downloadQueryLog: PropTypes.func,
getFilteringStatus: PropTypes.func,
filtering: PropTypes.object,
userRules: PropTypes.string,
setRules: PropTypes.func,
addSuccessToast: PropTypes.func,
processingRules: PropTypes.bool,
logStatusProcessing: PropTypes.bool,
t: PropTypes.func,
getLogs: PropTypes.func.isRequired,
queryLogs: PropTypes.object.isRequired,
dashboard: PropTypes.object.isRequired,
toggleLogStatus: PropTypes.func.isRequired,
downloadQueryLog: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object.isRequired,
setRules: PropTypes.func.isRequired,
addSuccessToast: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Logs);

View File

@ -253,3 +253,10 @@ export const ENCRYPTION_SOURCE = {
PATH: 'path',
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,
rule,
service_name,
status,
} = log;
const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => {
@ -44,6 +45,7 @@ export const normalizeLogs = logs => logs.map((log) => {
filterId,
rule,
serviceName: service_name,
status,
};
});