+ client: handle blocked services
This commit is contained in:
parent
3c684d1f85
commit
92cebd5b31
6
client/package-lock.json
generated
vendored
6
client/package-lock.json
generated
vendored
@ -945,9 +945,9 @@
|
||||
}
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
|
||||
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
|
2
client/package.json
vendored
2
client/package.json
vendored
@ -10,7 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nivo/line": "^0.49.1",
|
||||
"axios": "^0.18.1",
|
||||
"axios": "^0.19.0",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^1.29.0",
|
||||
"file-saver": "^1.3.8",
|
||||
|
@ -344,5 +344,14 @@
|
||||
"form_answer": "Enter IP address or domain name",
|
||||
"form_error_domain_format": "Invalid domain format",
|
||||
"form_error_answer_format": "Invalid answer format",
|
||||
"configure": "Configure"
|
||||
"configure": "Configure",
|
||||
"main_settings": "Main settings",
|
||||
"block_services": "Block specific services",
|
||||
"blocked_services": "Blocked services",
|
||||
"blocked_services_desc": "Allows to quickly block popular sites and services.",
|
||||
"blocked_services_saved": "Blocked services successfully saved",
|
||||
"blocked_services_global": "Use global blocked services",
|
||||
"blocked_service": "Blocked service",
|
||||
"block_all": "Block all",
|
||||
"unblock_all": "Unblock all"
|
||||
}
|
||||
|
37
client/src/actions/services.js
Normal file
37
client/src/actions/services.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './index';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
|
||||
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
||||
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
export const getBlockedServices = () => async (dispatch) => {
|
||||
dispatch(getBlockedServicesRequest());
|
||||
try {
|
||||
const data = await apiClient.getBlockedServices();
|
||||
dispatch(getBlockedServicesSuccess(data));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getBlockedServicesFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
|
||||
export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
|
||||
export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
export const setBlockedServices = values => async (dispatch) => {
|
||||
dispatch(setBlockedServicesRequest());
|
||||
try {
|
||||
await apiClient.setBlockedServices(values);
|
||||
dispatch(setBlockedServicesSuccess());
|
||||
dispatch(getBlockedServices());
|
||||
dispatch(addSuccessToast('blocked_services_saved'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setBlockedServicesFailure());
|
||||
}
|
||||
};
|
@ -509,4 +509,22 @@ export default class Api {
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Blocked services
|
||||
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
||||
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
||||
|
||||
getBlockedServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_LIST;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setBlockedServices(config) {
|
||||
const { path, method } = this.BLOCKED_SERVICES_SET;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,11 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__row--icons {
|
||||
max-width: 180px;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.logs__row .list-unstyled {
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
@ -26,6 +31,7 @@
|
||||
|
||||
.logs__text,
|
||||
.logs__row .list-unstyled li {
|
||||
padding: 0 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -8,6 +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 { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
@ -39,12 +40,8 @@ class Logs extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderTooltip(isFiltered, rule, filter) {
|
||||
if (rule) {
|
||||
return (isFiltered && <PopoverFiltered rule={rule} filter={filter}/>);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
renderTooltip = (isFiltered, rule, filter, service) =>
|
||||
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
|
||||
|
||||
toggleBlocking = (type, domain) => {
|
||||
const { userRules } = this.props.filtering;
|
||||
@ -146,6 +143,21 @@ class Logs extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (reason === 'FilteredBlockedService') {
|
||||
const getService = SERVICES
|
||||
.find(service => service.id === row.original.serviceName);
|
||||
const serviceName = getService && getService.name;
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<span className="logs__text" title={parsedFilteredReason}>
|
||||
{parsedFilteredReason}
|
||||
</span>
|
||||
{this.renderTooltip(isFiltered, '', '', serviceName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFiltered) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
|
@ -17,10 +17,19 @@ class ClientsTable extends Component {
|
||||
};
|
||||
|
||||
handleSubmit = (values) => {
|
||||
let config = values;
|
||||
|
||||
if (values && values.blocked_services) {
|
||||
const blocked_services = Object
|
||||
.keys(values.blocked_services)
|
||||
.filter(service => values.blocked_services[service]);
|
||||
config = { ...values, blocked_services };
|
||||
}
|
||||
|
||||
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||
this.handleFormUpdate(values, this.props.modalClientName);
|
||||
this.handleFormUpdate(config, this.props.modalClientName);
|
||||
} else {
|
||||
this.handleFormAdd(values);
|
||||
this.handleFormAdd(config);
|
||||
}
|
||||
};
|
||||
|
||||
@ -41,6 +50,7 @@ class ClientsTable extends Component {
|
||||
return {
|
||||
identifier,
|
||||
use_global_settings: true,
|
||||
use_global_blocked_services: true,
|
||||
...client,
|
||||
};
|
||||
}
|
||||
@ -48,6 +58,7 @@ class ClientsTable extends Component {
|
||||
return {
|
||||
identifier: CLIENT_ID.IP,
|
||||
use_global_settings: true,
|
||||
use_global_blocked_services: true,
|
||||
};
|
||||
};
|
||||
|
||||
@ -116,6 +127,27 @@ class ClientsTable extends Component {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('blocked_services'),
|
||||
accessor: 'blocked_services',
|
||||
Cell: (row) => {
|
||||
const { value, original } = row;
|
||||
|
||||
if (original.use_global_blocked_services) {
|
||||
return <Trans>settings_global</Trans>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--icons">
|
||||
{value && value.length > 0 ? value.map(service => (
|
||||
<svg className="service__icon service__icon--table" title={service} key={service}>
|
||||
<use xlinkHref={`#${service}`} />
|
||||
</svg>
|
||||
)) : '–'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_statistics'),
|
||||
accessor: 'statistics',
|
||||
|
@ -5,18 +5,46 @@ import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
|
||||
import { CLIENT_ID } from '../../../helpers/constants';
|
||||
import Tabs from '../../ui/Tabs';
|
||||
import { toggleAllServices } from '../../../helpers/helpers';
|
||||
import { renderField, renderRadioField, renderSelectField, renderServiceField, ipv4, mac, required } from '../../../helpers/form';
|
||||
import { CLIENT_ID, SERVICES } from '../../../helpers/constants';
|
||||
import './Service.css';
|
||||
|
||||
const settingsCheckboxes = [
|
||||
{
|
||||
name: 'use_global_settings',
|
||||
placeholder: 'client_global_settings',
|
||||
},
|
||||
{
|
||||
name: 'filtering_enabled',
|
||||
placeholder: 'block_domain_use_filters_and_hosts',
|
||||
},
|
||||
{
|
||||
name: 'safebrowsing_enabled',
|
||||
placeholder: 'use_adguard_browsing_sec',
|
||||
},
|
||||
{
|
||||
name: 'parental_enabled',
|
||||
placeholder: 'use_adguard_parental',
|
||||
},
|
||||
{
|
||||
name: 'safesearch_enabled',
|
||||
placeholder: 'enforce_safe_search',
|
||||
},
|
||||
];
|
||||
|
||||
let Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
reset,
|
||||
change,
|
||||
pristine,
|
||||
submitting,
|
||||
clientIdentifier,
|
||||
useGlobalSettings,
|
||||
useGlobalServices,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
@ -26,57 +54,70 @@ let Form = (props) => {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form__group">
|
||||
<div className="form-inline mb-3">
|
||||
<div className="form__inline mb-2">
|
||||
<strong className="mr-3">
|
||||
<Trans>client_identifier</Trans>
|
||||
</strong>
|
||||
<label className="mr-3">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
name="identifier"
|
||||
component={renderField}
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="ip"
|
||||
/>{' '}
|
||||
<Trans>ip_address</Trans>
|
||||
</label>
|
||||
<label>
|
||||
placeholder={t('ip_address')}
|
||||
/>
|
||||
<Field
|
||||
name="identifier"
|
||||
component={renderField}
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="mac"
|
||||
/>{' '}
|
||||
MAC
|
||||
</label>
|
||||
placeholder="MAC"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{clientIdentifier === CLIENT_ID.IP && (
|
||||
<div className="form__group">
|
||||
<div className="row">
|
||||
<div className="col col-sm-6">
|
||||
{clientIdentifier === CLIENT_ID.IP && (
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="ip"
|
||||
name="ip"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{clientIdentifier === CLIENT_ID.MAC && (
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="mac"
|
||||
name="mac"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_mac')}
|
||||
validate={[mac, required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col col-sm-6">
|
||||
<Field
|
||||
id="ip"
|
||||
name="ip"
|
||||
id="name"
|
||||
name="name"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[ipv4, required]}
|
||||
placeholder={t('form_client_name')}
|
||||
validate={[required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{clientIdentifier === CLIENT_ID.MAC && (
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="mac"
|
||||
name="mac"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_mac')}
|
||||
validate={[mac, required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form__desc">
|
||||
<Trans
|
||||
components={[
|
||||
@ -90,72 +131,67 @@ let Form = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="name"
|
||||
name="name"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_client_name')}
|
||||
validate={[required]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<strong>
|
||||
<Trans>settings</Trans>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="use_global_settings"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('client_global_settings')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="filtering_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('block_domain_use_filters_and_hosts')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="safebrowsing_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('use_adguard_browsing_sec')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="parental_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('use_adguard_parental')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="safesearch_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('enforce_safe_search')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
<Tabs controlClass="form">
|
||||
<div label="settings" title={props.t('main_settings')}>
|
||||
{settingsCheckboxes.map(setting => (
|
||||
<div className="form__group" key={setting.name}>
|
||||
<Field
|
||||
name={setting.name}
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t(setting.placeholder)}
|
||||
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div label="services" title={props.t('block_services')}>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="use_global_blocked_services"
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={t('blocked_services_global')}
|
||||
modifier="service--global"
|
||||
/>
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={useGlobalServices}
|
||||
onClick={() => toggleAllServices(SERVICES, change, true)}
|
||||
>
|
||||
<Trans>block_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={useGlobalServices}
|
||||
onClick={() => toggleAllServices(SERVICES, change, false)}
|
||||
>
|
||||
<Trans>unblock_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="services">
|
||||
{SERVICES.map(service => (
|
||||
<Field
|
||||
key={service.id}
|
||||
icon={service.id}
|
||||
name={`blocked_services.${service.id}`}
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={service.name}
|
||||
disabled={useGlobalServices}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
@ -188,10 +224,12 @@ Form.propTypes = {
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
clientIdentifier: PropTypes.string,
|
||||
useGlobalSettings: PropTypes.bool,
|
||||
useGlobalServices: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingUpdating: PropTypes.bool.isRequired,
|
||||
@ -202,9 +240,11 @@ const selector = formValueSelector('clientForm');
|
||||
Form = connect((state) => {
|
||||
const clientIdentifier = selector(state, 'identifier');
|
||||
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
||||
return {
|
||||
clientIdentifier,
|
||||
useGlobalSettings,
|
||||
useGlobalServices,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
|
@ -6,6 +6,24 @@ import ReactModal from 'react-modal';
|
||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
|
||||
const getInitialData = (initial) => {
|
||||
if (initial && initial.blocked_services) {
|
||||
const { blocked_services } = initial;
|
||||
const blocked = {};
|
||||
|
||||
blocked_services.forEach((service) => {
|
||||
blocked[service] = true;
|
||||
});
|
||||
|
||||
return {
|
||||
...initial,
|
||||
blocked_services: blocked,
|
||||
};
|
||||
}
|
||||
|
||||
return initial;
|
||||
};
|
||||
|
||||
const Modal = (props) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
@ -16,6 +34,7 @@ const Modal = (props) => {
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = props;
|
||||
const initialData = getInitialData(currentClientData);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
@ -38,9 +57,7 @@ const Modal = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
<Form
|
||||
initialValues={{
|
||||
...currentClientData,
|
||||
}}
|
||||
initialValues={{ ...initialData }}
|
||||
onSubmit={handleSubmit}
|
||||
toggleClientModal={toggleClientModal}
|
||||
processingAdding={processingAdding}
|
||||
|
79
client/src/components/Settings/Clients/Service.css
Normal file
79
client/src/components/Settings/Clients/Service.css
Normal file
@ -0,0 +1,79 @@
|
||||
.service {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.services {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.service {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||
max-width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||
width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||
}
|
||||
|
||||
.service--global {
|
||||
flex-basis: 1;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.service:nth-child(1n) {
|
||||
margin-right: 30px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.service:nth-child(3n) {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.service__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.service--global .service__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.service__icon--table {
|
||||
margin-bottom: 3px;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.service__switch {
|
||||
margin-left: auto;
|
||||
border: 1px solid rgba(150, 150, 150, 0.12);
|
||||
}
|
||||
|
||||
.custom-switch-input:checked ~ .service__switch {
|
||||
background-color: #cd201f;
|
||||
}
|
||||
|
||||
.custom-switch-input:focus ~ .service__switch {
|
||||
box-shadow: 0 0 0 2px #cd201f3b;
|
||||
border-color: #ec4241;
|
||||
}
|
||||
|
||||
.custom-switch-input:disabled ~ .service__switch,
|
||||
.custom-switch-input:disabled ~ .service__text,
|
||||
.custom-switch-input:disabled ~ .service__icon {
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
90
client/src/components/Settings/Services/Form.js
Normal file
90
client/src/components/Settings/Services/Form.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { toggleAllServices } from '../../../helpers/helpers';
|
||||
import { renderServiceField } from '../../../helpers/form';
|
||||
import { SERVICES } from '../../../helpers/constants';
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
change,
|
||||
pristine,
|
||||
submitting,
|
||||
processing,
|
||||
processingSet,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form__group">
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(SERVICES, change, true)}
|
||||
>
|
||||
<Trans>block_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(SERVICES, change, false)}
|
||||
>
|
||||
<Trans>unblock_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="services">
|
||||
{SERVICES.map(service => (
|
||||
<Field
|
||||
key={service.id}
|
||||
icon={service.id}
|
||||
name={`blocked_services.${service.id}`}
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={service.name}
|
||||
disabled={processing || processingSet}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || pristine || processing || processingSet}
|
||||
>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
processing: PropTypes.bool.isRequired,
|
||||
processingSet: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'servicesForm',
|
||||
enableReinitialize: true,
|
||||
}),
|
||||
])(Form);
|
69
client/src/components/Settings/Services/index.js
Normal file
69
client/src/components/Settings/Services/index.js
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
class Services extends Component {
|
||||
handleSubmit = (values) => {
|
||||
let config = values;
|
||||
|
||||
if (values && values.blocked_services) {
|
||||
const blocked_services = Object
|
||||
.keys(values.blocked_services)
|
||||
.filter(service => values.blocked_services[service]);
|
||||
config = blocked_services;
|
||||
}
|
||||
|
||||
this.props.setBlockedServices(config);
|
||||
};
|
||||
|
||||
|
||||
getInitialDataForServices = (initial) => {
|
||||
if (initial) {
|
||||
const blocked = {};
|
||||
|
||||
initial.forEach((service) => {
|
||||
blocked[service] = true;
|
||||
});
|
||||
|
||||
return {
|
||||
blocked_services: blocked,
|
||||
};
|
||||
}
|
||||
|
||||
return initial;
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const { services, t } = this.props;
|
||||
const initialData = this.getInitialDataForServices(services.list);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('blocked_services')}
|
||||
subtitle={t('Allows to quickly block popular sites.')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="form">
|
||||
<Form
|
||||
initialValues={{ ...initialData }}
|
||||
processing={services.processing}
|
||||
processingSet={services.processingSet}
|
||||
onSubmit={this.handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Services.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
services: PropTypes.object.isRequired,
|
||||
setBlockedServices: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Services);
|
@ -11,11 +11,20 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form__inline {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.btn-standard {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.form-control--textarea {
|
||||
min-height: 110px;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Services from './Services';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
@ -35,6 +36,7 @@ class Settings extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initSettings(this.settings);
|
||||
this.props.getBlockedServices();
|
||||
}
|
||||
|
||||
renderSettings = (settings) => {
|
||||
@ -59,7 +61,9 @@ class Settings extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, t } = this.props;
|
||||
const {
|
||||
settings, services, setBlockedServices, t,
|
||||
} = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('general_settings')} />
|
||||
@ -74,6 +78,12 @@ class Settings extends Component {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-md-12">
|
||||
<Services
|
||||
services={services}
|
||||
setBlockedServices={setBlockedServices}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
Binary file not shown.
@ -109,9 +109,11 @@
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: #9aa0ac;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.popover__icon--green {
|
||||
color: #66b574;
|
||||
stroke: #66b574;
|
||||
}
|
||||
|
||||
|
@ -6,19 +6,36 @@ import './Popover.css';
|
||||
|
||||
class PopoverFilter extends Component {
|
||||
render() {
|
||||
const { rule, filter, service } = this.props;
|
||||
|
||||
if (!rule && !service) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="popover-wrap">
|
||||
<div className="popover__trigger popover__trigger--filter">
|
||||
<svg className="popover__icon popover__icon--green" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
|
||||
<svg className="popover__icon popover__icon--green">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="popover__body popover__body--filter">
|
||||
<div className="popover__list">
|
||||
<div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>rule_label</Trans>: <strong>{this.props.rule}</strong>
|
||||
</div>
|
||||
{this.props.filter && <div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>filter_label</Trans>: <strong>{this.props.filter}</strong>
|
||||
</div>}
|
||||
{rule && (
|
||||
<div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>rule_label</Trans>: <strong>{rule}</strong>
|
||||
</div>
|
||||
)}
|
||||
{filter && (
|
||||
<div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>filter_label</Trans>: <strong>{filter}</strong>
|
||||
</div>
|
||||
)}
|
||||
{service && (
|
||||
<div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>blocked_service</Trans>: <strong>{service}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,8 +44,9 @@ class PopoverFilter extends Component {
|
||||
}
|
||||
|
||||
PopoverFilter.propTypes = {
|
||||
rule: PropTypes.string.isRequired,
|
||||
rule: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
service: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withNamespaces()(PopoverFilter);
|
||||
|
@ -6,6 +6,20 @@
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.tabs__controls--form {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tabs__controls--form .tab__control {
|
||||
min-width: initial;
|
||||
margin-right: 25px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tabs__controls--form .tab__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab__control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import Tab from './Tab';
|
||||
import './Tabs.css';
|
||||
@ -16,6 +17,7 @@ class Tabs extends Component {
|
||||
render() {
|
||||
const {
|
||||
props: {
|
||||
controlClass,
|
||||
children,
|
||||
},
|
||||
state: {
|
||||
@ -23,9 +25,14 @@ class Tabs extends Component {
|
||||
},
|
||||
} = this;
|
||||
|
||||
const getControlClass = classnames({
|
||||
tabs__controls: true,
|
||||
[`tabs__controls--${controlClass}`]: controlClass,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div className="tabs__controls">
|
||||
<div className={getControlClass}>
|
||||
{children.map((child) => {
|
||||
const { label, title } = child.props;
|
||||
|
||||
@ -54,6 +61,7 @@ class Tabs extends Component {
|
||||
}
|
||||
|
||||
Tabs.propTypes = {
|
||||
controlClass: PropTypes.string,
|
||||
children: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { initSettings, toggleSetting } from '../actions';
|
||||
import { getBlockedServices, setBlockedServices } from '../actions/services';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { settings } = state;
|
||||
const { settings, services } = state;
|
||||
const props = {
|
||||
settings,
|
||||
services,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
@ -13,6 +15,8 @@ const mapStateToProps = (state) => {
|
||||
const mapDispatchToProps = {
|
||||
initSettings,
|
||||
toggleSetting,
|
||||
getBlockedServices,
|
||||
setBlockedServices,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
@ -181,3 +181,66 @@ export const CLIENT_ID = {
|
||||
};
|
||||
|
||||
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];
|
||||
|
||||
export const SERVICES = [
|
||||
{
|
||||
id: 'facebook',
|
||||
name: 'Facebook',
|
||||
},
|
||||
{
|
||||
id: 'whatsapp',
|
||||
name: 'WhatsApp',
|
||||
},
|
||||
{
|
||||
id: 'instagram',
|
||||
name: 'Instagram',
|
||||
},
|
||||
{
|
||||
id: 'twitter',
|
||||
name: 'Twitter',
|
||||
},
|
||||
{
|
||||
id: 'youtube',
|
||||
name: 'YouTube',
|
||||
},
|
||||
{
|
||||
id: 'netflix',
|
||||
name: 'Netflix',
|
||||
},
|
||||
{
|
||||
id: 'snapchat',
|
||||
name: 'Snapchat',
|
||||
},
|
||||
{
|
||||
id: 'messenger',
|
||||
name: 'Messenger',
|
||||
},
|
||||
{
|
||||
id: 'twitch',
|
||||
name: 'Twitch',
|
||||
},
|
||||
{
|
||||
id: 'discord',
|
||||
name: 'Discord',
|
||||
},
|
||||
{
|
||||
id: 'skype',
|
||||
name: 'Skype',
|
||||
},
|
||||
{
|
||||
id: 'steam',
|
||||
name: 'Steam',
|
||||
},
|
||||
{
|
||||
id: 'ok',
|
||||
name: 'OK',
|
||||
},
|
||||
{
|
||||
id: 'vk',
|
||||
name: 'VK',
|
||||
},
|
||||
{
|
||||
id: 'mail_ru',
|
||||
name: 'mail.ru',
|
||||
},
|
||||
];
|
||||
|
@ -27,6 +27,23 @@ export const renderField = ({
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const renderRadioField = ({
|
||||
input, placeholder, disabled, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<label className="custom-control custom-radio custom-control-inline">
|
||||
<input
|
||||
{...input}
|
||||
type="radio"
|
||||
className="custom-control-input"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className="custom-control-label">{placeholder}</span>
|
||||
</label>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const renderSelectField = ({
|
||||
input, placeholder, disabled, meta: { touched, error },
|
||||
}) => (
|
||||
@ -46,6 +63,28 @@ export const renderSelectField = ({
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const renderServiceField = ({
|
||||
input, placeholder, disabled, modifier, icon, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<label className={`service custom-switch ${modifier}`}>
|
||||
<input
|
||||
{...input}
|
||||
type="checkbox"
|
||||
className="custom-switch-input"
|
||||
value={placeholder.toLowerCase()}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className="service__switch custom-switch-indicator"></span>
|
||||
<span className="service__text">{placeholder}</span>
|
||||
<svg className="service__icon">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
</label>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
|
@ -27,6 +27,7 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
client,
|
||||
filterId,
|
||||
rule,
|
||||
service_name,
|
||||
} = log;
|
||||
const { host: domain, type } = question;
|
||||
const responsesArray = response ? response.map((response) => {
|
||||
@ -42,6 +43,7 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
client,
|
||||
filterId,
|
||||
rule,
|
||||
serviceName: service_name,
|
||||
};
|
||||
});
|
||||
|
||||
@ -225,3 +227,7 @@ export const sortClients = (clients) => {
|
||||
|
||||
return clients.sort(compare);
|
||||
};
|
||||
|
||||
export const toggleAllServices = (services, change, isSelected) => {
|
||||
services.forEach(service => change(`blocked_services.${service.id}`, isSelected));
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import encryption from './encryption';
|
||||
import clients from './clients';
|
||||
import access from './access';
|
||||
import rewrites from './rewrites';
|
||||
import services from './services';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
@ -424,6 +425,7 @@ export default combineReducers({
|
||||
clients,
|
||||
access,
|
||||
rewrites,
|
||||
services,
|
||||
loadingBar: loadingBarReducer,
|
||||
form: formReducer,
|
||||
});
|
||||
|
29
client/src/reducers/services.js
Normal file
29
client/src/reducers/services.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import * as actions from '../actions/services';
|
||||
|
||||
const services = handleActions(
|
||||
{
|
||||
[actions.getBlockedServicesRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getBlockedServicesFailure]: state => ({ ...state, processing: false }),
|
||||
[actions.getBlockedServicesSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
list: payload,
|
||||
processing: false,
|
||||
}),
|
||||
|
||||
[actions.setBlockedServicesRequest]: state => ({ ...state, processingSet: true }),
|
||||
[actions.setBlockedServicesFailure]: state => ({ ...state, processingSet: false }),
|
||||
[actions.setBlockedServicesSuccess]: state => ({
|
||||
...state,
|
||||
processingSet: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
processing: true,
|
||||
processingSet: false,
|
||||
list: [],
|
||||
},
|
||||
);
|
||||
|
||||
export default services;
|
Loading…
Reference in New Issue
Block a user