+ client: Add choosing filter lists

Fix #1325

Squashed commit of the following:

commit d8f7de72226855a961051e09b4b78f4dd71baadd
Merge: f9bbe861 36f3218b
Author: Andrey Meshkov <am@adguard.com>
Date:   Mon Jul 6 19:34:53 2020 +0300

    Merge branch 'master' into feature/1325

commit f9bbe861c9dbd631b5708f8eb073270b83a3f70f
Merge: 99710fef 4f8138bd
Author: Andrey Meshkov <am@adguard.com>
Date:   Mon Jul 6 19:33:53 2020 +0300

    Merge branch 'master' into feature/1325

commit 99710fef0825966b224e4a30a979e4d45f929af1
Merge: 8329326d a5380ead
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 6 18:04:32 2020 +0300

    Merge branch 'feature/1325' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/1325

commit 8329326d6470dfcf2cdc4479e0290f7cc56ddca4
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 6 18:03:56 2020 +0300

    Update locales, add title for select modal

commit a5380ead56d15eba3f36c38f8fc0eedc89c2c57a
Author: Andrey Meshkov <am@adguard.com>
Date:   Mon Jul 6 17:26:37 2020 +0300

    Update readme

commit dfe6e254d909ee6994cacef53d417bb073dfd802
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 6 13:44:19 2020 +0300

    Change info icon width

commit 06120cf3da9065fc9cc3a2864b976563d4cfe06a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 6 13:38:58 2020 +0300

    Review changes

commit ae3c6cacc5610a0f95bec2f6ef8a63e90041e4dd
Merge: dd56a3bb 73c5d9ea
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jul 6 12:01:57 2020 +0300

    Merge branch 'master' into feature/1325

commit dd56a3bbb851687823242fa653cc3bb63dedf5e4
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Jul 3 15:52:01 2020 +0300

    Added blocklists

commit f08f0eb0cdd8cd488d3a8f1182854b72775cf06e
Merge: 854d4f88 21dfb5ff
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Jul 3 14:06:19 2020 +0300

    Merge branch 'master' into feature/1325

commit 854d4f88017a33dc7f788835dc98591cec9b213f
Merge: 23946266 2c47053c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jun 22 14:09:31 2020 +0300

    Merge branch 'master' into feature/1325

commit 23946266d4913479bcecfcb7702a096983d20685
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue May 26 19:00:26 2020 +0300

    Math filters by url

commit 661e0482f01ffea0d0f5aa81b3b253143d0ca112
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon May 25 21:07:21 2020 +0300

    Change data format

commit ac4ff483b6b06ec0be49a41b5ddd3329f4ae2bbb
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu May 14 19:52:45 2020 +0300

    + client: Add choosing filter lists
This commit is contained in:
Andrey Meshkov
2020-07-06 19:58:44 +03:00
parent 36f3218b1c
commit 49646cf706
29 changed files with 617 additions and 132 deletions

View File

@@ -53,6 +53,15 @@ body {
}
}
.modal-body--medium {
max-height: 20rem;
overflow-y: scroll;
}
.modal-body__item:not(:first-child) {
padding-top: 1.5rem;
}
.font-monospace {
font-family: var(--font-family-monospace);
}

View File

@@ -21,7 +21,7 @@ class DnsAllowlist extends Component {
const { filtering } = this.props;
const whitelist = true;
if (filtering.modalType === MODAL_TYPE.EDIT) {
if (filtering.modalType === MODAL_TYPE.EDIT_FILTERS) {
this.props.editFilter(filtering.modalFilterUrl, values, whitelist);
} else {
this.props.addFilter(url, name, whitelist);
@@ -44,6 +44,10 @@ class DnsAllowlist extends Component {
this.props.refreshFilters({ whitelist: true });
};
openAddFiltersModal = () => {
this.props.toggleFilteringModal({ type: MODAL_TYPE.ADD_FILTERS });
};
render() {
const {
t,
@@ -92,7 +96,7 @@ class DnsAllowlist extends Component {
whitelist={whitelist}
/>
<Actions
handleAdd={() => toggleFilteringModal({ type: MODAL_TYPE.ADD })}
handleAdd={this.openAddFiltersModal}
handleRefresh={this.handleRefresh}
processingRefreshFilters={processingRefreshFilters}
whitelist={whitelist}
@@ -102,8 +106,9 @@ class DnsAllowlist extends Component {
</div>
</div>
<Modal
filters={whitelistFilters}
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
toggleFilteringModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}

View File

@@ -1,4 +1,4 @@
import React, { Component, Fragment } from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
@@ -6,24 +6,47 @@ import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Modal from './Modal';
import Actions from './Actions';
import Table from './Table';
import Table from './Table';
import { MODAL_TYPE } from '../../helpers/constants';
import { getCurrentFilter } from '../../helpers/helpers';
import {
getCurrentFilter,
getObjDiff,
} from '../../helpers/helpers';
const filtersCatalog = require('../../helpers/filters/filters.json');
class DnsBlocklist extends Component {
componentDidMount() {
this.props.getFilteringStatus();
}
handleSubmit = (values) => {
const { name, url } = values;
const { filtering } = this.props;
handleSubmit = (values, _, { initialValues }) => {
const { filtering: { modalFilterUrl, modalType } } = this.props;
if (filtering.modalType === MODAL_TYPE.EDIT) {
this.props.editFilter(filtering.modalFilterUrl, values);
} else {
this.props.addFilter(url, name);
switch (modalType) {
case MODAL_TYPE.EDIT_FILTERS:
this.props.editFilter(modalFilterUrl, values);
break;
case MODAL_TYPE.ADD_FILTERS: {
const { name, url } = values;
this.props.addFilter(url, name);
break;
}
case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
const changedValues = getObjDiff(initialValues, values);
Object.keys(changedValues)
.forEach((fieldName) => {
// filterId is actually in the field name
const { source, name } = filtersCatalog.filters[fieldName];
this.props.addFilter(source, name);
});
break;
}
default:
break;
}
};
@@ -41,6 +64,10 @@ class DnsBlocklist extends Component {
this.props.refreshFilters({ whitelist: false });
};
openSelectTypeModal = () => {
this.props.toggleFilteringModal({ type: MODAL_TYPE.SELECT_MODAL_TYPE });
};
render() {
const {
t,
@@ -67,7 +94,7 @@ class DnsBlocklist extends Component {
|| processingRefreshFilters;
return (
<Fragment>
<>
<PageTitle
title={t('dns_blocklists')}
subtitle={t('dns_blocklists_desc')}
@@ -85,7 +112,7 @@ class DnsBlocklist extends Component {
toggleFilter={this.toggleFilter}
/>
<Actions
handleAdd={() => toggleFilteringModal({ type: MODAL_TYPE.ADD })}
handleAdd={this.openSelectTypeModal}
handleRefresh={this.handleRefresh}
processingRefreshFilters={processingRefreshFilters}
/>
@@ -94,8 +121,10 @@ class DnsBlocklist extends Component {
</div>
</div>
<Modal
filtersCatalog={filtersCatalog}
filters={filters}
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
toggleFilteringModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}
@@ -104,7 +133,7 @@ class DnsBlocklist extends Component {
modalType={modalType}
currentFilterData={currentFilterData}
/>
</Fragment>
</>
);
}
}

View File

@@ -1,11 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { renderInputField } from '../../helpers/form';
import classNames from 'classnames';
import { validatePath, validateRequiredValue } from '../../helpers/validators';
import { FORM_NAME } from '../../helpers/constants';
import { renderInputField, renderSelectField } from '../../helpers/form';
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
const getIconsData = (homepage, source) => ([
{
iconName: 'dashboard',
href: homepage,
className: 'ml-1',
},
{
iconName: 'info',
href: source,
},
]);
const renderIcons = (iconsData) => iconsData.map(({
iconName,
href,
className = '',
}) => <a key={iconName} href={href} target="_blank" rel="noopener noreferrer"
className={classNames('d-flex align-items-center', className)}
>
<svg className="nav-icon nav-icon--gray">
<use xlinkHref={`#${iconName}`} />
</svg>
</a>);
const renderFilters = ({ categories, filters }, selectedSources, t) => Object.keys(categories)
.map((categoryId) => {
const category = categories[categoryId];
const categoryFilters = [];
Object.keys(filters)
.sort()
.forEach((key) => {
const filter = filters[key];
filter.id = key;
if (filter.categoryId === categoryId) {
categoryFilters.push(filter);
}
});
return <div key={category.name} className="modal-body__item">
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
<p className="mb-3">{t(category.description)}</p>
{categoryFilters.map((filter) => {
const { homepage, source, name } = filter;
const isSelected = Object.prototype.hasOwnProperty.call(selectedSources, source);
const iconsData = getIconsData(homepage, source);
return <div key={name} className="d-flex align-items-center pb-1">
<Field
name={`${filter.id}`}
type="checkbox"
component={renderSelectField}
placeholder={t(name)}
disabled={isSelected}
checked={isSelected}
/>
{renderIcons(iconsData)}
</div>;
})}
</div>;
});
const Form = (props) => {
const {
@@ -15,11 +79,38 @@ const Form = (props) => {
processingAddFilter,
processingConfigFilter,
whitelist,
modalType,
toggleFilteringModal,
selectedSources,
filtersCatalog,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
const openModal = (modalType, timeout = MODAL_OPEN_TIMEOUT) => {
toggleFilteringModal();
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
};
const openFilteringListModal = () => openModal(MODAL_TYPE.CHOOSE_FILTERING_LIST);
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
return <form onSubmit={handleSubmit}>
<div className="modal-body modal-body--medium">
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE
&& <div className="d-flex justify-content-around">
<button onClick={openFilteringListModal}
className="btn btn-success btn-standard mr-2 btn-large">
{t('choose_from_list')}
</button>
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
{t('add_custom_list')}
</button>
</div>}
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST
&& renderFilters(filtersCatalog, selectedSources, t)}
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST
&& modalType !== MODAL_TYPE.SELECT_MODAL_TYPE
&& <>
<div className="form__group">
<Field
id="name"
@@ -45,28 +136,27 @@ const Form = (props) => {
/>
</div>
<div className="form__description">
{whitelist ? <Trans>enter_valid_allowlist</Trans>
: <Trans>enter_valid_blocklist</Trans>}
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={closeModal}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success"
disabled={processingAddFilter || processingConfigFilter}
>
<Trans>save_btn</Trans>
</button>
</div>
</form>
);
</>}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={closeModal}
>
{t('cancel_btn')}
</button>
<button
type="submit"
className="btn btn-success"
disabled={processingAddFilter || processingConfigFilter}
>
{t('save_btn')}
</button>
</div>
</form>;
};
Form.propTypes = {
@@ -76,6 +166,10 @@ Form.propTypes = {
processingAddFilter: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
whitelist: PropTypes.bool,
modalType: PropTypes.string.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
filtersCatalog: PropTypes.object,
selectedSources: PropTypes.object,
};
export default flow([

View File

@@ -1,17 +1,51 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import { Trans, withTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import { MODAL_TYPE } from '../../helpers/constants';
import Form from './Form';
import '../ui/Modal.css';
import { getMap } from '../../helpers/helpers';
ReactModal.setAppElement('#root');
const MODAL_TYPE_TO_TITLE_TYPE_MAP = {
[MODAL_TYPE.EDIT_FILTERS]: 'edit',
[MODAL_TYPE.ADD_FILTERS]: 'new',
[MODAL_TYPE.SELECT_MODAL_TYPE]: 'new',
[MODAL_TYPE.CHOOSE_FILTERING_LIST]: 'choose',
};
/**
* @param modalType {'EDIT_FILTERS' | 'ADD_FILTERS' | 'CHOOSE_FILTERING_LIST'}
* @param whitelist {boolean}
* @returns {'new_allowlist' | 'edit_allowlist' | 'choose_allowlist' |
* 'new_blocklist' | 'edit_blocklist' | 'choose_blocklist' | null}
*/
const getTitle = (modalType, whitelist) => {
const titleType = MODAL_TYPE_TO_TITLE_TYPE_MAP[modalType];
if (!titleType) {
return null;
}
return `${titleType}_${whitelist ? 'allowlist' : 'blocklist'}`;
};
const getSelectedValues = (filters, catalogSourcesToIdMap) => filters.reduce((acc, { url }) => {
if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) {
const fieldId = `filter${catalogSourcesToIdMap[url]}`;
acc.selectedFilterIds[fieldId] = true;
acc.selectedSources[url] = true;
}
return acc;
}, {
selectedFilterIds: {},
selectedSources: {},
});
class Modal extends Component {
closeModal = () => {
this.props.toggleModal();
this.props.toggleFilteringModal();
};
render() {
@@ -23,19 +57,30 @@ class Modal extends Component {
modalType,
currentFilterData,
whitelist,
toggleFilteringModal,
filters,
t,
filtersCatalog,
} = this.props;
const newListTitle = whitelist ? (
<Trans>new_allowlist</Trans>
) : (
<Trans>new_blocklist</Trans>
);
let initialValues;
let selectedSources;
switch (modalType) {
case MODAL_TYPE.EDIT_FILTERS:
initialValues = currentFilterData;
break;
case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
const catalogSourcesToIdMap = getMap(Object.values(filtersCatalog.filters), 'source', 'id');
const editListTitle = whitelist ? (
<Trans>edit_allowlist</Trans>
) : (
<Trans>edit_blocklist</Trans>
);
const selectedValues = getSelectedValues(filters, catalogSourcesToIdMap);
initialValues = selectedValues.selectedFilterIds;
selectedSources = selectedValues.selectedSources;
break;
}
default:
}
const title = t(getTitle(modalType, whitelist));
return (
<ReactModal
@@ -46,24 +91,22 @@ class Modal extends Component {
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT ? (
editListTitle
) : (
newListTitle
)}
</h4>
{title && <h4 className="modal-title">{title}</h4>}
<button type="button" className="close" onClick={this.closeModal}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{ ...currentFilterData }}
selectedSources={selectedSources}
initialValues={initialValues}
filtersCatalog={filtersCatalog}
modalType={modalType}
onSubmit={handleSubmit}
processingAddFilter={processingAddFilter}
processingConfigFilter={processingConfigFilter}
closeModal={this.closeModal}
whitelist={whitelist}
toggleFilteringModal={toggleFilteringModal}
/>
</div>
</ReactModal>
@@ -72,7 +115,7 @@ class Modal extends Component {
}
Modal.propTypes = {
toggleModal: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
addFilter: PropTypes.func.isRequired,
isFilterAdded: PropTypes.bool.isRequired,
@@ -83,6 +126,8 @@ Modal.propTypes = {
currentFilterData: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
whitelist: PropTypes.bool,
filters: PropTypes.array.isRequired,
filtersCatalog: PropTypes.object,
};
export default withTranslation()(Modal);

View File

@@ -92,7 +92,7 @@ class Table extends Component {
className="btn btn-icon btn-outline-primary btn-sm mr-2"
title={t('edit_table_action')}
onClick={() => toggleFilteringModal({
type: MODAL_TYPE.EDIT,
type: MODAL_TYPE.EDIT_FILTERS,
url: value,
})
}

View File

@@ -160,6 +160,10 @@
display: none;
}
.nav-icon--gray {
color: #9aa0ac;
}
.header-brand-img {
height: 32px;
}

View File

@@ -41,7 +41,7 @@ class ClientsTable extends Component {
}
}
if (this.props.modalType === MODAL_TYPE.EDIT) {
if (this.props.modalType === MODAL_TYPE.EDIT_FILTERS) {
this.handleFormUpdate(config, this.props.modalClientName);
} else {
this.handleFormAdd(config);
@@ -221,7 +221,7 @@ class ClientsTable extends Component {
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => toggleClientModal({
type: MODAL_TYPE.EDIT,
type: MODAL_TYPE.EDIT_FILTERS,
name: clientName,
})
}
@@ -306,7 +306,7 @@ class ClientsTable extends Component {
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}
disabled={processingAdding}
>
<Trans>client_add</Trans>

View File

@@ -47,7 +47,7 @@ const Modal = (props) => {
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT ? (
{modalType === MODAL_TYPE.EDIT_FILTERS ? (
<Trans>client_edit</Trans>
) : (
<Trans>client_new</Trans>

View File

@@ -335,6 +335,15 @@ const Icons = () => (
fillRule="evenodd" strokeLinecap="round" />
</svg>
</symbol>
<symbol id="info" viewBox="0 0 24 24" fill="currentColor"
strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path
d="M64 1C29.3 1 1 29.3 1 64s28.3 63 63 63 63-28.3 63-63S98.7 1 64 1zm0 118C33.7 119 9 94.3 9 64S33.7 9 64 9s55 24.7 55 55-24.7 55-55 55z" />
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
</svg>
</symbol>
</svg>
);