Merge: fix #822 - Whitelist filter rules

Squashed commit of the following:

commit 350c6d5fadd77145b801df8887284bf4d64fbd19
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Wed Feb 26 15:43:29 2020 +0300

    * client: update translations

commit a884dffcd59f2259e2eee2c1e5a3270819bf8962
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Mon Feb 17 17:32:10 2020 +0300

    + client: handle whitelist filters

commit a586ec5bc614ffb0e01584a1fbdc7292b4865e68
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Jan 29 18:16:59 2020 +0300

    + client: add whitelist

commit a52c3de62cf2fa34be6394771fb8bb56b4ee81e3
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Feb 20 17:50:44 2020 +0300

    * change /filtering/refresh

commit 7f8f2ecccb9f7fa65318c1717dc6a7bd61afccf4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Feb 20 16:17:07 2020 +0300

    * fix race-detector issue

commit ac4b64c4a52c5b364a4b154bf18dea0fdf45647f
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Jan 20 20:08:21 2020 +0300

    + whitelist filters
This commit is contained in:
Andrey Meshkov
2020-02-26 19:58:25 +03:00
parent 82aa38fce3
commit d839136fee
70 changed files with 1821 additions and 1132 deletions

View File

@@ -12,7 +12,11 @@ import './index.css';
import Header from '../../containers/Header';
import Dashboard from '../../containers/Dashboard';
import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import CustomRules from '../../containers/CustomRules';
import DnsBlocklist from '../../containers/DnsBlocklist';
import DnsAllowlist from '../../containers/DnsAllowlist';
import DnsRewrites from '../../containers/DnsRewrites';
import Dns from '../../containers/Dns';
import Encryption from '../../containers/Encryption';
@@ -108,7 +112,10 @@ class App extends Component {
<Route path="/encryption" component={Encryption} />
<Route path="/dhcp" component={Dhcp} />
<Route path="/clients" component={Clients} />
<Route path="/filters" component={Filters} />
<Route path="/filters" component={DnsBlocklist} />
<Route path="/dns_allowlists" component={DnsAllowlist} />
<Route path="/dns_rewrites" component={DnsRewrites} />
<Route path="/custom_rules" component={CustomRules} />
<Route path="/logs" component={Logs} />
<Route path="/guide" component={SetupGuide} />
</Fragment>

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
const Actions = ({
handleAdd, handleRefresh, processingRefreshFilters, whitelist,
}) => (
<div className="card-actions">
<button
className="btn btn-success btn-standard mr-2 btn-large"
type="submit"
onClick={handleAdd}
>
{whitelist ? (
<Trans>add_allowlist</Trans>
) : (
<Trans>add_blocklist</Trans>
)}
</button>
<button
className="btn btn-primary btn-standard"
type="submit"
onClick={handleRefresh}
disabled={processingRefreshFilters}
>
<Trans>check_updates_btn</Trans>
</button>
</div>
);
Actions.propTypes = {
handleAdd: PropTypes.func.isRequired,
handleRefresh: PropTypes.func.isRequired,
processingRefreshFilters: PropTypes.bool.isRequired,
whitelist: PropTypes.bool,
};
export default withNamespaces()(Actions);

View File

@@ -14,12 +14,13 @@ import {
} from '../../../helpers/helpers';
import { FILTERED } from '../../../helpers/constants';
const getFilterName = (id, filters, t) => {
const getFilterName = (id, filters, whitelistFilters, t) => {
if (id === 0) {
return t('filtered_custom_rules');
}
const filter = filters.find(filter => filter.id === id);
const filter = filters.find(filter => filter.id === id)
|| whitelistFilters.find(filter => filter.id === id);
if (filter && filter.name) {
return t('query_log_filtered', { filter: filter.name });
@@ -43,14 +44,9 @@ const getTitle = (reason, filterName, t, onlyFiltered) => {
if (checkWhiteList(reason)) {
return (
<Fragment>
<div>
{t('host_whitelisted')}
</div>
<div>
{filterName}
</div>
</Fragment>
<div>
{filterName}
</div>
);
}
@@ -90,6 +86,7 @@ const getColor = (reason) => {
const Info = ({
filters,
whitelistFilters,
hostname,
reason,
filter_id,
@@ -99,7 +96,7 @@ const Info = ({
ip_addrs,
t,
}) => {
const filterName = getFilterName(filter_id, filters, t);
const filterName = getFilterName(filter_id, filters, whitelistFilters, t);
const onlyFiltered = checkSafeSearch(reason)
|| checkSafeBrowsing(reason)
|| checkParental(reason);
@@ -149,6 +146,7 @@ const Info = ({
Info.propTypes = {
filters: PropTypes.array.isRequired,
whitelistFilters: PropTypes.array.isRequired,
hostname: PropTypes.string.isRequired,
reason: PropTypes.string.isRequired,
filter_id: PropTypes.number,

View File

@@ -17,6 +17,7 @@ const Check = (props) => {
processing,
check,
filters,
whitelistFilters,
} = props;
const {
@@ -62,6 +63,7 @@ const Check = (props) => {
<hr/>
<Info
filters={filters}
whitelistFilters={whitelistFilters}
hostname={hostname}
reason={reason}
filter_id={filter_id}
@@ -87,6 +89,7 @@ Check.propTypes = {
processing: PropTypes.bool.isRequired,
check: PropTypes.object.isRequired,
filters: PropTypes.array.isRequired,
whitelistFilters: PropTypes.array.isRequired,
};
export default flow([

View File

@@ -0,0 +1,95 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import PageTitle from '../ui/PageTitle';
import Examples from './Examples';
import Check from './Check';
class CustomRules extends Component {
componentDidMount() {
this.props.getFilteringStatus();
}
handleChange = (e) => {
const { value } = e.currentTarget;
this.handleRulesChange(value);
};
handleSubmit = (e) => {
e.preventDefault();
this.handleRulesSubmit();
};
handleRulesChange = (value) => {
this.props.handleRulesChange({ userRules: value });
};
handleRulesSubmit = () => {
this.props.setRules(this.props.filtering.userRules);
};
handleCheck = (values) => {
this.props.checkHost(values);
}
render() {
const {
t,
filtering: {
filters,
whitelistFilters,
userRules,
processingCheck,
check,
},
} = this.props;
return (
<Fragment>
<PageTitle title={t('custom_filtering_rules')} />
<Card
subtitle={t('custom_filter_rules_hint')}
>
<form onSubmit={this.handleSubmit}>
<textarea
className="form-control form-control--textarea-large"
value={userRules}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr />
<Examples />
</Card>
<Check
filters={filters}
whitelistFilters={whitelistFilters}
check={check}
onSubmit={this.handleCheck}
processing={processingCheck}
/>
</Fragment>
);
}
}
CustomRules.propTypes = {
filtering: PropTypes.object.isRequired,
setRules: PropTypes.func.isRequired,
checkHost: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(CustomRules);

View File

@@ -0,0 +1,130 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Modal from './Modal';
import Actions from './Actions';
import Table from './Table';
import { MODAL_TYPE } from '../../helpers/constants';
import { getCurrentFilter } from '../../helpers/helpers';
class DnsAllowlist extends Component {
componentDidMount() {
this.props.getFilteringStatus();
}
handleSubmit = (values) => {
const { name, url } = values;
const { filtering } = this.props;
const whitelist = true;
if (filtering.modalType === MODAL_TYPE.EDIT) {
this.props.editFilter(filtering.modalFilterUrl, values, whitelist);
} else {
this.props.addFilter(url, name, whitelist);
}
};
handleDelete = (url) => {
if (window.confirm(this.props.t('list_confirm_delete'))) {
const whitelist = true;
this.props.removeFilter(url, whitelist);
}
};
toggleFilter = (url, data) => {
const whitelist = true;
this.props.toggleFilterStatus(url, data, whitelist);
};
render() {
const {
t,
toggleFilteringModal,
refreshFilters,
addFilter,
toggleFilterStatus,
filtering: {
whitelistFilters,
isModalOpen,
isFilterAdded,
processingRefreshFilters,
processingRemoveFilter,
processingAddFilter,
processingConfigFilter,
processingFilters,
modalType,
modalFilterUrl,
},
} = this.props;
const currentFilterData = getCurrentFilter(modalFilterUrl, whitelistFilters);
const loading = processingFilters
|| processingAddFilter
|| processingRemoveFilter
|| processingRefreshFilters;
const whitelist = true;
return (
<Fragment>
<PageTitle
title={t('dns_allowlists')}
subtitle={t('dns_allowlists_desc')}
/>
<div className="content">
<div className="row">
<div className="col-md-12">
<Card subtitle={t('filters_and_hosts_hint')}>
<Table
filters={whitelistFilters}
loading={loading}
processingConfigFilter={processingConfigFilter}
toggleFilteringModal={toggleFilteringModal}
toggleFilterStatus={toggleFilterStatus}
handleDelete={this.handleDelete}
toggleFilter={this.toggleFilter}
whitelist={whitelist}
/>
<Actions
handleAdd={() => toggleFilteringModal({ type: MODAL_TYPE.ADD })}
handleRefresh={refreshFilters}
processingRefreshFilters={processingRefreshFilters}
whitelist={whitelist}
/>
</Card>
</div>
</div>
</div>
<Modal
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}
processingConfigFilter={processingConfigFilter}
handleSubmit={this.handleSubmit}
modalType={modalType}
currentFilterData={currentFilterData}
whitelist={whitelist}
/>
</Fragment>
);
}
}
DnsAllowlist.propTypes = {
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object.isRequired,
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(DnsAllowlist);

View File

@@ -0,0 +1,121 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Modal from './Modal';
import Actions from './Actions';
import Table from './Table';
import { MODAL_TYPE } from '../../helpers/constants';
import { getCurrentFilter } from '../../helpers/helpers';
class DnsBlocklist extends Component {
componentDidMount() {
this.props.getFilteringStatus();
}
handleSubmit = (values) => {
const { name, url } = values;
const { filtering } = this.props;
if (filtering.modalType === MODAL_TYPE.EDIT) {
this.props.editFilter(filtering.modalFilterUrl, values);
} else {
this.props.addFilter(url, name);
}
};
handleDelete = (url) => {
if (window.confirm(this.props.t('list_confirm_delete'))) {
this.props.removeFilter(url);
}
};
toggleFilter = (url, data) => {
this.props.toggleFilterStatus(url, data);
};
render() {
const {
t,
toggleFilteringModal,
refreshFilters,
addFilter,
filtering: {
filters,
isModalOpen,
isFilterAdded,
processingRefreshFilters,
processingRemoveFilter,
processingAddFilter,
processingConfigFilter,
processingFilters,
modalType,
modalFilterUrl,
},
} = this.props;
const currentFilterData = getCurrentFilter(modalFilterUrl, filters);
const loading = processingFilters
|| processingAddFilter
|| processingRemoveFilter
|| processingRefreshFilters;
return (
<Fragment>
<PageTitle
title={t('dns_blocklists')}
subtitle={t('dns_blocklists_desc')}
/>
<div className="content">
<div className="row">
<div className="col-md-12">
<Card subtitle={t('filters_and_hosts_hint')}>
<Table
filters={filters}
loading={loading}
processingConfigFilter={processingConfigFilter}
toggleFilteringModal={toggleFilteringModal}
handleDelete={this.handleDelete}
toggleFilter={this.toggleFilter}
/>
<Actions
handleAdd={() => toggleFilteringModal({ type: MODAL_TYPE.ADD })}
handleRefresh={refreshFilters}
processingRefreshFilters={processingRefreshFilters}
/>
</Card>
</div>
</div>
</div>
<Modal
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}
processingConfigFilter={processingConfigFilter}
handleSubmit={this.handleSubmit}
modalType={modalType}
currentFilterData={currentFilterData}
/>
</Fragment>
);
}
}
DnsBlocklist.propTypes = {
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object.isRequired,
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(DnsBlocklist);

View File

@@ -0,0 +1,54 @@
import React, { Fragment } from 'react';
import { withNamespaces, Trans } from 'react-i18next';
const Examples = () => (
<Fragment>
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>||example.org^</code> &nbsp;
<Trans>example_meaning_filter_block</Trans>
</li>
<li>
<code> @@||example.org^</code> &nbsp;
<Trans>example_meaning_filter_whitelist</Trans>
</li>
<li>
<code>127.0.0.1 example.org</code> &nbsp;
<Trans>example_meaning_host_block</Trans>
</li>
<li>
<code><Trans>example_comment</Trans></code> &nbsp;
<Trans>example_comment_meaning</Trans>
</li>
<li>
<code><Trans>example_comment_hash</Trans></code> &nbsp;
<Trans>example_comment_meaning</Trans>
</li>
<li>
<code>/REGEX/</code> &nbsp;
<Trans>example_regex_meaning</Trans>
</li>
</ol>
</div>
<p className="mt-1">
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists"
target="_blank"
rel="noopener noreferrer"
key="0"
>
link
</a>,
]}
>
filtering_rules_learn_more
</Trans>
</p>
</Fragment>
);
export default withNamespaces()(Examples);

View File

@@ -13,6 +13,7 @@ const Form = (props) => {
handleSubmit,
processingAddFilter,
processingConfigFilter,
whitelist,
} = props;
return (
@@ -41,7 +42,11 @@ const Form = (props) => {
/>
</div>
<div className="form__description">
<Trans>enter_valid_filter_url</Trans>
{whitelist ? (
<Trans>enter_valid_allowlist</Trans>
) : (
<Trans>enter_valid_blocklist</Trans>
)}
</div>
</div>
<div className="modal-footer">
@@ -70,6 +75,7 @@ Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
whitelist: PropTypes.bool,
};
export default flow([

View File

@@ -22,8 +22,21 @@ class Modal extends Component {
handleSubmit,
modalType,
currentFilterData,
whitelist,
} = this.props;
const newListTitle = whitelist ? (
<Trans>new_allowlist</Trans>
) : (
<Trans>new_blocklist</Trans>
);
const editListTitle = whitelist ? (
<Trans>edit_allowlist</Trans>
) : (
<Trans>edit_blocklist</Trans>
);
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
@@ -35,9 +48,9 @@ class Modal extends Component {
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT ? (
<Trans>edit_filter_title</Trans>
editListTitle
) : (
<Trans>new_filter_btn</Trans>
newListTitle
)}
</h4>
<button type="button" className="close" onClick={this.closeModal}>
@@ -50,6 +63,7 @@ class Modal extends Component {
processingAddFilter={processingAddFilter}
processingConfigFilter={processingConfigFilter}
closeModal={this.closeModal}
whitelist={whitelist}
/>
</div>
</ReactModal>
@@ -68,6 +82,7 @@ Modal.propTypes = {
modalType: PropTypes.string.isRequired,
currentFilterData: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
whitelist: PropTypes.bool,
};
export default withNamespaces()(Modal);

View File

@@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { renderInputField, required, domain, answer } from '../../../../helpers/form';
import { renderInputField, required, domain, answer } from '../../../helpers/form';
const Form = (props) => {
const {

View File

@@ -0,0 +1,92 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Table from './Table';
import Modal from './Modal';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
class Rewrites extends Component {
componentDidMount() {
this.props.getRewritesList();
}
handleSubmit = (values) => {
this.props.addRewrite(values);
};
handleDelete = (values) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
this.props.deleteRewrite(values);
}
};
render() {
const {
t,
rewrites,
toggleRewritesModal,
} = this.props;
const {
list,
isModalOpen,
processing,
processingAdd,
processingDelete,
} = rewrites;
return (
<Fragment>
<PageTitle
title={t('dns_rewrites')}
subtitle={t('rewrite_desc')}
/>
<Card
id="rewrites"
bodyType="card-body box-body--settings"
>
<Fragment>
<Table
list={list}
processing={processing}
processingAdd={processingAdd}
processingDelete={processingDelete}
handleDelete={this.handleDelete}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleRewritesModal()}
disabled={processingAdd}
>
<Trans>rewrite_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
toggleRewritesModal={toggleRewritesModal}
handleSubmit={this.handleSubmit}
processingAdd={processingAdd}
processingDelete={processingDelete}
/>
</Fragment>
</Card>
</Fragment>
);
}
}
Rewrites.propTypes = {
t: PropTypes.func.isRequired,
getRewritesList: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
addRewrite: PropTypes.func.isRequired,
deleteRewrite: PropTypes.func.isRequired,
rewrites: PropTypes.object.isRequired,
};
export default withNamespaces()(Rewrites);

View File

@@ -0,0 +1,157 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { withNamespaces, Trans } from 'react-i18next';
import CellWrap from '../ui/CellWrap';
import { MODAL_TYPE } from '../../helpers/constants';
import { formatDetailedDateTime } from '../../helpers/helpers';
class Table extends Component {
getDateCell = row => CellWrap(row, formatDetailedDateTime);
renderCheckbox = ({ original }) => {
const { processingConfigFilter, toggleFilter } = this.props;
const { url, name, enabled } = original;
const data = { name, url, enabled: !enabled };
return (
<label className="checkbox">
<input
type="checkbox"
className="checkbox__input"
onChange={() => toggleFilter(url, data)}
checked={enabled}
disabled={processingConfigFilter}
/>
<span className="checkbox__label" />
</label>
);
};
columns = [
{
Header: <Trans>enabled_table_header</Trans>,
accessor: 'enabled',
Cell: this.renderCheckbox,
width: 90,
className: 'text-center',
},
{
Header: <Trans>name_table_header</Trans>,
accessor: 'name',
minWidth: 200,
Cell: CellWrap,
},
{
Header: <Trans>list_url_table_header</Trans>,
accessor: 'url',
minWidth: 200,
Cell: ({ value }) => (
<div className="logs__row logs__row--overflow">
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="link logs__text"
>
{value}
</a>
</div>
),
},
{
Header: <Trans>rules_count_table_header</Trans>,
accessor: 'rulesCount',
className: 'text-center',
minWidth: 100,
Cell: props => props.value.toLocaleString(),
},
{
Header: <Trans>last_time_updated_table_header</Trans>,
accessor: 'lastUpdated',
className: 'text-center',
minWidth: 150,
Cell: this.getDateCell,
},
{
Header: <Trans>actions_table_header</Trans>,
accessor: 'url',
className: 'text-center',
width: 100,
sortable: false,
Cell: (row) => {
const { value } = row;
const { t, toggleFilteringModal, handleDelete } = this.props;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
title={t('edit_table_action')}
onClick={() =>
toggleFilteringModal({
type: MODAL_TYPE.EDIT,
url: value,
})
}
>
<svg className="icons">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete(value)}
title={t('delete_table_action')}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
);
},
},
];
render() {
const {
loading, filters, t, whitelist,
} = this.props;
return (
<ReactTable
data={filters}
columns={this.columns}
showPagination={true}
defaultPageSize={10}
loading={loading}
minRows={6}
previousText={t('previous_btn')}
nextText={t('next_btn')}
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')}
/>
);
}
}
Table.propTypes = {
filters: PropTypes.array.isRequired,
loading: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
toggleFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
whitelist: PropTypes.bool,
};
export default withNamespaces()(Table);

View File

@@ -1,95 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
class UserRules extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleRulesChange(value);
};
handleSubmit = (e) => {
e.preventDefault();
this.props.handleRulesSubmit();
};
render() {
const { t, userRules } = this.props;
return (
<Card title={t('custom_filter_rules')} subtitle={t('custom_filter_rules_hint')}>
<form onSubmit={this.handleSubmit}>
<textarea
className="form-control form-control--textarea-large"
value={userRules}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr />
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>||example.org^</code> &nbsp;
<Trans>example_meaning_filter_block</Trans>
</li>
<li>
<code> @@||example.org^</code> &nbsp;
<Trans>example_meaning_filter_whitelist</Trans>
</li>
<li>
<code>127.0.0.1 example.org</code> &nbsp;
<Trans>example_meaning_host_block</Trans>
</li>
<li>
<code><Trans>example_comment</Trans></code> &nbsp;
<Trans>example_comment_meaning</Trans>
</li>
<li>
<code><Trans>example_comment_hash</Trans></code> &nbsp;
<Trans>example_comment_meaning</Trans>
</li>
<li>
<code>/REGEX/</code> &nbsp;
<Trans>example_regex_meaning</Trans>
</li>
</ol>
</div>
<p className="mt-1">
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists"
target="_blank"
rel="noopener noreferrer"
key="0"
>
link
</a>,
]}
>
filtering_rules_learn_more
</Trans>
</p>
</Card>
);
}
}
UserRules.propTypes = {
userRules: PropTypes.string.isRequired,
handleRulesChange: PropTypes.func.isRequired,
handleRulesSubmit: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(UserRules);

View File

@@ -1,305 +0,0 @@
import React, { Component, Fragment } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import CellWrap from '../ui/CellWrap';
import UserRules from './UserRules';
import Modal from './Modal';
import Check from './Check';
import { formatDetailedDateTime } from '../../helpers/helpers';
import { MODAL_TYPE } from '../../helpers/constants';
class Filters extends Component {
componentDidMount() {
this.props.getFilteringStatus();
}
handleRulesChange = (value) => {
this.props.handleRulesChange({ userRules: value });
};
handleRulesSubmit = () => {
this.props.setRules(this.props.filtering.userRules);
};
handleSubmit = (values) => {
const { name, url } = values;
const { filtering } = this.props;
if (filtering.modalType === MODAL_TYPE.EDIT) {
const data = { ...values };
this.props.editFilter(filtering.modalFilterUrl, data);
} else {
this.props.addFilter(url, name);
}
}
renderCheckbox = ({ original }) => {
const { processingConfigFilter } = this.props.filtering;
const { url, name, enabled } = original;
const data = { name, url, enabled: !enabled };
return (
<label className="checkbox">
<input
type="checkbox"
className="checkbox__input"
onChange={() => this.props.toggleFilterStatus(url, data)}
checked={enabled}
disabled={processingConfigFilter}
/>
<span className="checkbox__label" />
</label>
);
};
handleDelete = (url) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('filter_confirm_delete'))) {
this.props.removeFilter({ url });
}
};
getDateCell = row => CellWrap(row, formatDetailedDateTime);
getFilter = (url, filters) => {
const filter = filters.find(item => url === item.url);
if (filter) {
const { enabled, name, url } = filter;
return { enabled, name, url };
}
return { name: '', url: '' };
};
handleCheck = (values) => {
this.props.checkHost(values);
}
columns = [
{
Header: <Trans>enabled_table_header</Trans>,
accessor: 'enabled',
Cell: this.renderCheckbox,
width: 90,
className: 'text-center',
},
{
Header: <Trans>name_table_header</Trans>,
accessor: 'name',
minWidth: 200,
Cell: CellWrap,
},
{
Header: <Trans>filter_url_table_header</Trans>,
accessor: 'url',
minWidth: 200,
Cell: ({ value }) => (
<div className="logs__row logs__row--overflow">
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="link logs__text"
>
{value}
</a>
</div>
),
},
{
Header: <Trans>rules_count_table_header</Trans>,
accessor: 'rulesCount',
className: 'text-center',
minWidth: 100,
Cell: props => props.value.toLocaleString(),
},
{
Header: <Trans>last_time_updated_table_header</Trans>,
accessor: 'lastUpdated',
className: 'text-center',
minWidth: 150,
Cell: this.getDateCell,
},
{
Header: <Trans>actions_table_header</Trans>,
accessor: 'url',
className: 'text-center',
width: 100,
sortable: false,
Cell: (row) => {
const { value } = row;
const { t, toggleFilteringModal } = this.props;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
title={t('edit_table_action')}
onClick={() =>
toggleFilteringModal({
type: MODAL_TYPE.EDIT,
url: value,
})
}
>
<svg className="icons">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.handleDelete(value)}
title={this.props.t('delete_table_action')}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
);
},
},
];
render() {
const {
filtering, t, toggleFilteringModal, refreshFilters, addFilter,
} = this.props;
const {
filters,
userRules,
isModalOpen,
isFilterAdded,
processingRefreshFilters,
processingRemoveFilter,
processingAddFilter,
processingConfigFilter,
processingFilters,
modalType,
modalFilterUrl,
processingCheck,
check,
} = filtering;
const currentFilterData = this.getFilter(modalFilterUrl, filters);
return (
<Fragment>
<PageTitle title={t('filters')} />
<div className="content">
<div className="row">
<div className="col-md-12">
<Card
title={t('filters_and_hosts')}
subtitle={t('filters_and_hosts_hint')}
>
<ReactTable
data={filters}
columns={this.columns}
showPagination={true}
defaultPageSize={10}
loading={
processingFilters ||
processingAddFilter ||
processingRemoveFilter ||
processingRefreshFilters
}
minRows={4}
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText="/"
rowsText={t('rows_table_footer_text')}
noDataText={t('no_filters_added')}
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard mr-2 btn-large"
type="submit"
onClick={() =>
toggleFilteringModal({ type: MODAL_TYPE.ADD })
}
>
<Trans>add_filter_btn</Trans>
</button>
<button
className="btn btn-primary btn-standard"
type="submit"
onClick={refreshFilters}
disabled={processingRefreshFilters}
>
<Trans>check_updates_btn</Trans>
</button>
</div>
</Card>
</div>
<div className="col-md-12">
<UserRules
userRules={userRules}
handleRulesChange={this.handleRulesChange}
handleRulesSubmit={this.handleRulesSubmit}
/>
</div>
<div className="col-md-12">
<Check
filters={filters}
check={check}
onSubmit={this.handleCheck}
processing={processingCheck}
/>
</div>
</div>
</div>
<Modal
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}
processingConfigFilter={processingConfigFilter}
handleSubmit={this.handleSubmit}
modalType={modalType}
currentFilterData={currentFilterData}
/>
</Fragment>
);
}
}
Filters.propTypes = {
setRules: PropTypes.func,
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.shape({
userRules: PropTypes.string.isRequired,
filters: PropTypes.array.isRequired,
isModalOpen: PropTypes.bool.isRequired,
isFilterAdded: PropTypes.bool.isRequired,
processingFilters: PropTypes.bool.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
processingRefreshFilters: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
processingRemoveFilter: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
processingCheck: PropTypes.bool.isRequired,
}),
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
checkHost: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Filters);

View File

@@ -5,33 +5,39 @@ import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import { SETTINGS_URLS } from '../../helpers/constants';
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
import Dropdown from '../ui/Dropdown';
const MENU_ITEMS = [
{
route: '', exact: true, xlinkHref: 'dashboard', text: 'dashboard', order: 0,
route: MENU_URLS.root, exact: true, icon: 'dashboard', text: 'dashboard', order: 0,
},
// Settings dropdown should have visual order 1
// Filters dropdown should have visual order 2
{
route: 'filters', xlinkHref: 'filters', text: 'filters', order: 2,
route: MENU_URLS.logs, icon: 'log', text: 'query_log', order: 3,
},
{
route: 'logs', xlinkHref: 'log', text: 'query_log', order: 3,
},
{
route: 'guide', xlinkHref: 'setup', text: 'setup_guide', order: 4,
route: MENU_URLS.guide, icon: 'setup', text: 'setup_guide', order: 4,
},
];
const DROPDOWN_ITEMS = [
{ route: 'settings', text: 'general_settings' },
{ route: 'dns', text: 'dns_settings' },
{ route: 'encryption', text: 'encryption_settings' },
{ route: 'clients', text: 'client_settings' },
{ route: 'dhcp', text: 'dhcp_settings' },
const SETTINGS_ITEMS = [
{ route: SETTINGS_URLS.settings, text: 'general_settings' },
{ route: SETTINGS_URLS.dns, text: 'dns_settings' },
{ route: SETTINGS_URLS.encryption, text: 'encryption_settings' },
{ route: SETTINGS_URLS.clients, text: 'client_settings' },
{ route: SETTINGS_URLS.dhcp, text: 'dhcp_settings' },
];
const FILTERS_ITEMS = [
{ route: FILTERS_URLS.dns_blocklists, text: 'dns_blocklists' },
{ route: FILTERS_URLS.dns_allowlists, text: 'dns_allowlists' },
{ route: FILTERS_URLS.dns_rewrites, text: 'dns_rewrites' },
{ route: FILTERS_URLS.custom_rules, text: 'custom_filtering_rules' },
];
class Menu extends Component {
@@ -43,50 +49,82 @@ class Menu extends Component {
this.props.toggleMenuOpen();
};
getActiveClassForSettings = () => {
getActiveClassForDropdown = (URLS) => {
const { pathname } = this.props.location;
const isSettingsPage = SETTINGS_URLS.some(item => item === pathname);
const isActivePage = Object.values(URLS).some(item => item === pathname);
return isSettingsPage ? 'active' : '';
return isActivePage ? 'active' : '';
};
getNavLink = ({
route, exact, text, order, className, icon,
}) => (
<NavLink
to={route}
key={route}
exact={exact || false}
className={`order-${order} ${className}`}
onClick={this.toggleMenu}
>
{icon && (
<svg className="nav-icon">
<use xlinkHref={`#${icon}`} />
</svg>
)}
<Trans>{text}</Trans>
</NavLink>
);
getDropdown = ({
label, order, URLS, icon, ITEMS,
}) =>
(
<Dropdown
label={this.props.t(label)}
baseClassName={`dropdown nav-item order-${order}`}
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
icon={icon}>
{ITEMS.map(item => (
this.getNavLink({
...item,
order,
className: 'dropdown-item',
})))}
</Dropdown>
);
render() {
const menuClass = classnames({
'header__column mobile-menu': true,
'mobile-menu--active': this.props.isMenuOpen,
});
const dropdownControlClass = `nav-link ${this.getActiveClassForSettings()}`;
return (
<Fragment>
<div className={menuClass}>
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
{MENU_ITEMS.map(({
route, text, exact, xlinkHref, order,
}) => (
<li className={`nav-item order-${order}`} key={text} onClick={this.toggleMenu}>
<NavLink to={`/${route}`} exact={exact || false} className="nav-link">
<svg className="nav-icon">
<use xlinkHref={`#${xlinkHref}`} />
</svg>
<Trans>{text}</Trans>
</NavLink>
{MENU_ITEMS.map(item => (
<li
className={`nav-item order-${item.order}`}
key={item.text}
onClick={this.toggleMenu}
>
{this.getNavLink({ ...item, className: 'nav-link' })}
</li>
))}
<Dropdown
label={this.props.t('settings')}
baseClassName="dropdown nav-item order-1"
controlClassName={dropdownControlClass}
icon="settings"
>
{DROPDOWN_ITEMS.map(({ route, text }) => (
<NavLink to={`/${route}`} className="dropdown-item" key={text}
onClick={this.toggleMenu}>
<Trans>{text}</Trans>
</NavLink>
))}
</Dropdown>
{this.getDropdown({
order: 1,
label: 'settings',
icon: 'settings',
URLS: SETTINGS_URLS,
ITEMS: SETTINGS_ITEMS,
})}
{this.getDropdown({
order: 2,
label: 'filters',
icon: 'filters',
URLS: FILTERS_URLS,
ITEMS: FILTERS_ITEMS,
})}
</ul>
</div>
</Fragment>

View File

@@ -140,12 +140,13 @@ class Logs extends Component {
})
);
getFilterName = (filters, filterId, t) => {
getFilterName = (filters, whitelistFilters, filterId, t) => {
if (filterId === CUSTOM_FILTERING_RULES_ID) {
return t('custom_filter_rules');
}
const filter = filters.find(filter => filter.id === filterId);
const filter = filters.find(filter => filter.id === filterId)
|| whitelistFilters.find(filter => filter.id === filterId);
let filterName = '';
if (filter) {
@@ -164,7 +165,7 @@ class Logs extends Component {
reason, filterId, rule, status, originalAnswer,
} = original;
const { t, filtering } = this.props;
const { filters } = filtering;
const { filters, whitelistFilters } = filtering;
const isFiltered = checkFiltered(reason);
const isBlackList = checkBlackList(reason);
@@ -177,7 +178,7 @@ class Logs extends Component {
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, filterId, t);
const filterName = this.getFilterName(filters, whitelistFilters, filterId, t);
if (isBlockedCnameIp) {
const normalizedAnswer = this.normalizeResponse(originalAnswer);
@@ -188,6 +189,7 @@ class Logs extends Component {
<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)}
@@ -242,7 +244,7 @@ class Logs extends Component {
</div>
{isRewrite ? (
<div className="logs__action">
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
<Link to="/dns_rewrites" className="btn btn-sm btn-outline-primary">
<Trans>configure</Trans>
</Link>
</div>

View File

@@ -1,83 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Table from './Table';
import Modal from './Modal';
import Card from '../../../ui/Card';
class Rewrites extends Component {
handleSubmit = (values) => {
this.props.addRewrite(values);
};
handleDelete = (values) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
this.props.deleteRewrite(values);
}
};
render() {
const {
t,
rewrites,
toggleRewritesModal,
} = this.props;
const {
list,
isModalOpen,
processing,
processingAdd,
processingDelete,
} = rewrites;
return (
<Card
id="rewrites"
title={t('dns_rewrites')}
subtitle={t('rewrite_desc')}
bodyType="card-body box-body--settings"
>
<Fragment>
<Table
list={list}
processing={processing}
processingAdd={processingAdd}
processingDelete={processingDelete}
handleDelete={this.handleDelete}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleRewritesModal()}
disabled={processingAdd}
>
<Trans>rewrite_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
toggleRewritesModal={toggleRewritesModal}
handleSubmit={this.handleSubmit}
processingAdd={processingAdd}
processingDelete={processingDelete}
/>
</Fragment>
</Card>
);
}
}
Rewrites.propTypes = {
t: PropTypes.func.isRequired,
getRewritesList: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
addRewrite: PropTypes.func.isRequired,
deleteRewrite: PropTypes.func.isRequired,
rewrites: PropTypes.object.isRequired,
};
export default withNamespaces()(Rewrites);

View File

@@ -4,7 +4,6 @@ import { withNamespaces } from 'react-i18next';
import Upstream from './Upstream';
import Access from './Access';
import Rewrites from './Rewrites';
import Config from './Config';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
@@ -13,7 +12,6 @@ class Dns extends Component {
componentDidMount() {
this.props.getDnsSettings();
this.props.getAccessList();
this.props.getRewritesList();
this.props.getDnsConfig();
}
@@ -23,25 +21,18 @@ class Dns extends Component {
dashboard,
settings,
access,
rewrites,
setAccessList,
testUpstream,
setUpstream,
getRewritesList,
addRewrite,
deleteRewrite,
toggleRewritesModal,
dnsConfig,
setDnsConfig,
} = this.props;
const isDataLoading = dashboard.processingDnsSettings
|| access.processing
|| rewrites.processing
|| dnsConfig.processingGetConfig;
const isDataReady = !dashboard.processingDnsSettings
&& !access.processing
&& !rewrites.processing
&& !dnsConfig.processingGetConfig;
return (
@@ -64,13 +55,6 @@ class Dns extends Component {
testUpstream={testUpstream}
/>
<Access access={access} setAccessList={setAccessList} />
<Rewrites
rewrites={rewrites}
getRewritesList={getRewritesList}
addRewrite={addRewrite}
deleteRewrite={deleteRewrite}
toggleRewritesModal={toggleRewritesModal}
/>
</Fragment>
)}
</Fragment>
@@ -86,11 +70,6 @@ Dns.propTypes = {
getAccessList: PropTypes.func.isRequired,
setAccessList: PropTypes.func.isRequired,
access: PropTypes.object.isRequired,
rewrites: PropTypes.object.isRequired,
getRewritesList: PropTypes.func.isRequired,
addRewrite: PropTypes.func.isRequired,
deleteRewrite: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
getDnsSettings: PropTypes.func.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,

View File

@@ -3,28 +3,36 @@ import PropTypes from 'prop-types';
import './Card.css';
const Card = props => (
<div className={props.type ? `card ${props.type}` : 'card'} id={props.id ? props.id : ''}>
{props.title &&
<div className="card-header with-border">
<div className="card-inner">
<div className="card-title">
{props.title}
const Card = ({
type, id, title, subtitle, refresh, bodyType, children,
}) => (
<div className={type ? `card ${type}` : 'card'} id={id || ''}>
{(title || subtitle) && (
<div className="card-header with-border">
<div className="card-inner">
{title && (
<div className="card-title">
{title}
</div>
)}
{subtitle && (
<div
className="card-subtitle"
dangerouslySetInnerHTML={{ __html: subtitle }}
/>
)}
</div>
{props.subtitle &&
<div className="card-subtitle" dangerouslySetInnerHTML={{ __html: props.subtitle }} />
}
{refresh && (
<div className="card-options">
{refresh}
</div>
)}
</div>
{props.refresh &&
<div className="card-options">
{props.refresh}
</div>
}
</div>}
<div className={props.bodyType ? props.bodyType : 'card-body'}>
{props.children}
)}
<div className={bodyType || 'card-body'}>
{children}
</div>
</div>
);

View File

@@ -1,8 +1,17 @@
.page-header {
flex-direction: column;
align-items: flex-start;
}
.page-subtitle {
margin-left: 0.7rem;
margin-left: 0;
font-size: 0.9rem;
}
.page-title {
line-height: 2.2rem;
}
.page-title__actions {
display: block;
}

View File

@@ -7,9 +7,13 @@ const PageTitle = props => (
<div className="page-header">
<h1 className="page-title">
{props.title}
{props.subtitle && <span className="page-subtitle">{props.subtitle}</span>}
{props.children}
</h1>
{props.subtitle && (
<div className="page-subtitle">
{props.subtitle}
</div>
)}
</div>
);

View File

@@ -28,7 +28,7 @@ class PopoverFilter extends Component {
)}
{filter && (
<div className="popover__list-item popover__list-item--nowrap">
<Trans>filter_label</Trans>: <strong>{filter}</strong>
<Trans>list_label</Trans>: <strong>{filter}</strong>
</div>
)}
{service && (

View File

@@ -10143,6 +10143,7 @@ body.fixed-header .page {
display: flex;
-ms-flex-align: center;
align-items: center;
flex-direction: column;
margin: 1.5rem 0 1.5rem;
-ms-flex-wrap: wrap;
flex-wrap: wrap;